From 0c7b878bbe0b492a4d7d13f0a4d2317e1dba02a0 Mon Sep 17 00:00:00 2001
From: Joseph Walton-Rivers <joseph@walton-rivers.uk>
Date: Sun, 30 Oct 2022 15:32:51 +0000
Subject: [PATCH] improve missing texture rendering

---
 demo/data/redbook/debug_frag.glsl  |  73 ++++++++++++++++--
 demo/data/redbook/debug_vert.glsl  |  18 +++--
 demo/data/rollball.yml             |   6 ++
 demo/demo/GameScene.cpp            |  52 +++++++------
 demo/demo/models/viewer.cpp        |  13 ++++
 demo/demo/topdown.cpp              |  14 ++++
 fggl/data/assimp/module.cpp        |  10 +++
 fggl/data/procedural.cpp           |  51 +++++++------
 fggl/gfx/ogl/renderer.cpp          |   2 +-
 fggl/gfx/ogl4/meshes.cpp           | 118 ++++++++++++-----------------
 fggl/gfx/ogl4/models.cpp           |   2 +-
 fggl/gfx/ogl4/module.cpp           |  80 ++++++++++++-------
 fggl/scenes/game.cpp               |   7 +-
 include/fggl/data/procedural.hpp   |  11 +--
 include/fggl/gfx/ogl4/fallback.hpp |   1 +
 include/fggl/gfx/ogl4/meshes.hpp   |  30 ++++++--
 include/fggl/gfx/ogl4/models.hpp   |   1 +
 include/fggl/gfx/phong.hpp         |  21 +++++
 include/fggl/mesh/mesh.hpp         |  27 ++++++-
 include/fggl/scenes/game.hpp       |   2 +
 vendor/imgui/CMakeLists.txt        |   2 +-
 21 files changed, 366 insertions(+), 175 deletions(-)

diff --git a/demo/data/redbook/debug_frag.glsl b/demo/data/redbook/debug_frag.glsl
index 299da0c..050be21 100644
--- a/demo/data/redbook/debug_frag.glsl
+++ b/demo/data/redbook/debug_frag.glsl
@@ -1,19 +1,80 @@
 /**
  * OpenGL RedBook Shader.
  * Examples 7.8, 7.9 and 7.10.
+ *
+ * Reflections are happening in camera space
  */
 #version 330 core
 
-in vec4 Position;
-in vec3 Normal;
-in vec4 Colour;
-in vec2 TexPos;
+in Vertex {
+    vec3 Position;
+    vec3 Normal;
+    vec3 Colour;
+    vec2 TexPos;
+};
 
 out vec4 FragColour;
 
+const float constant = 1.0;
+const float linear = 0.022;
+const float quadratic = 0.0019;
+
+float specPower = 0.5;
+const float shininess = 8;
+
 uniform sampler2D diffuseTexture;
+uniform sampler2D specularTexture;
+
+uniform mat4 MVMatrix;
+uniform vec3 viewerPos_ws;
+
+struct DirectionalLight {
+    vec3 direction;
+    vec3 ambient;
+    vec3 diffuse;
+    vec3 specular;
+};
+
+uniform DirectionalLight light;
+
+const int hasPos = 0;
+
+vec4 calcDirLight(DirectionalLight light, vec3 Normal, vec3 viewDir, vec4 specPx, vec4 diffPx) {
+    vec3 lightDir = normalize( ( vec4(light.direction, 1) * MVMatrix).xyz - Position.xyz );
+    vec4 ambient = 0.1 * vec4( light.ambient, 1);
+
+    vec3 reflectDir = reflect( -lightDir, Normal );
+    float spec = pow( max( dot(viewDir, reflectDir), 0.0), shininess);
+    vec4 specular = 1.0 * vec4( light.specular, 1) * specPower * (spec * specPx);
+
+    float diff = max( dot(Normal, lightDir), 0.0 );
+    vec4 diffuse = vec4(light.diffuse,1) * (diff * diffPx);
+
+    if ( hasPos == 1 ) {
+        vec3 lightPos = ( vec4(light.direction, 1) * MVMatrix).xyz;
+        float distance = length( lightPos - Position.xyz );
+        float att = 1.0 / (constant +
+            (linear * distance) +
+            (quadratic * (distance * distance)));
+
+        ambient *= att;
+        diffuse *= att;
+        specular *= att;
+    }
+
+    return (ambient + diffuse + specular);
+}
 
 void main() {
-    vec3 normalScale = 0.5 + (Normal / 2);
-    FragColour = Colour * vec4(normalScale, 1) * texture(diffuseTexture, TexPos);
+    vec3 viewDir = normalize(-Position);
+
+    vec4 diffPx = vec4(1, 1, 1, 1);
+    vec4 specPx = vec4(1, 1, 1, 1);
+    if ( hasPos != 1) {
+        diffPx = texture(diffuseTexture, TexPos);
+        specPx = texture(specularTexture, TexPos);
+    }
+
+    FragColour = vec4(Colour, 1);
+    FragColour *= calcDirLight(light, Normal, viewDir, specPx, diffPx);
 }
\ No newline at end of file
diff --git a/demo/data/redbook/debug_vert.glsl b/demo/data/redbook/debug_vert.glsl
index cf0e341..2026662 100644
--- a/demo/data/redbook/debug_vert.glsl
+++ b/demo/data/redbook/debug_vert.glsl
@@ -4,6 +4,7 @@
  */
 #version 330 core
 
+
 layout (location = 0) in vec3 VertexPosition;
 layout (location = 1) in vec3 VertexNormal;
 layout (location = 2) in vec3 VertexColour;
@@ -13,15 +14,18 @@ uniform mat4 MVPMatrix;
 uniform mat4 MVMatrix;
 uniform mat3 NormalMatrix;
 
-out vec4 Position;
-out vec3 Normal;
-out vec4 Colour;
-out vec2 TexPos;
+out Vertex {
+    vec3 Position;
+    vec3 Normal;
+    vec3 Colour;
+    vec2 TexPos;
+};
 
 void main() {
-    Colour = vec4(VertexColour, 1);
-    Normal = NormalMatrix * VertexNormal;
+    Colour = VertexColour;
+    Normal = mat3(transpose(inverse(MVMatrix))) * VertexNormal;
     TexPos = VertexTex;
-    Position = MVMatrix * vec4(VertexPosition, 1);
+    Position = vec3(MVMatrix * vec4(VertexPosition, 1));
+
     gl_Position = MVPMatrix * vec4(VertexPosition, 1);
 }
\ No newline at end of file
diff --git a/demo/data/rollball.yml b/demo/data/rollball.yml
index 5c562bc..8eec78b 100644
--- a/demo/data/rollball.yml
+++ b/demo/data/rollball.yml
@@ -89,6 +89,11 @@ prefabs:
         type: kinematic
         shape:
           type: box
+  - name: rb_light
+    components:
+      Transform:
+      gfx::phong::directional:
+        direction: [10, 5, 0]
 scene:
   - prefab: rb_wallX
     components:
@@ -124,5 +129,6 @@ scene:
         origin: [6, -0.5, -15]
   - prefab: rb_player
     name: "player"
+  - prefab: rb_light
 scripts:
   - "rollball.lua"
\ No newline at end of file
diff --git a/demo/demo/GameScene.cpp b/demo/demo/GameScene.cpp
index 0ce3b09..4923d15 100644
--- a/demo/demo/GameScene.cpp
+++ b/demo/demo/GameScene.cpp
@@ -24,6 +24,7 @@
 
 #include "GameScene.h"
 #include "fggl/entity/loader/loader.hpp"
+#include "fggl/mesh/components.hpp"
 
 camera_type cam_mode = cam_free;
 
@@ -128,7 +129,8 @@ static void setupBunkerPrototype(fggl::entity::EntityFactory* factory) {
 
 		// mesh
 		int nSections = 2;
-		fggl::data::Mesh mesh;
+		fggl::mesh::MultiMesh3D mesh;
+
 		for (int j=-(nSections/2); j<=nSections/2; j++) {
 			const auto shapeOffset = glm::vec3( 0.0f, 0.5f, (float)j * 1.0f );
 
@@ -140,37 +142,41 @@ static void setupBunkerPrototype(fggl::entity::EntityFactory* factory) {
 				glm::vec3( 1.0f, 0.0f, 0.0f) + shapeOffset,
 				glm::vec3( 0.0f, fggl::math::HALF_PI, 0.0f) );
 
-			fggl::data::make_cube( mesh, cubeMat );
-			fggl::data::make_slope( mesh, leftSlope );
-			fggl::data::make_slope( mesh, rightSlope );
+			fggl::data::make_cube( mesh.generate(), cubeMat);
+			fggl::data::make_slope( mesh.generate(), leftSlope );
+			fggl::data::make_slope( mesh.generate(), rightSlope );
 		}
-		mesh.removeDups();
+		//mesh.removeDups();
 
 		// generate mesh component data
 		// FIXME: find a better way to do this, avoid re-uploading the whole mesh.
 		fggl::entity::ComponentSpec procMesh{};
 		procMesh.set<std::string>("pipeline", "redbook/debug");
 
-		YAML::Node vertexData;
-		for (auto& vertex : mesh.vertexList()) {
-			YAML::Node vertexNode;
-			vertexNode["position"] = vertex.posititon;
-			vertexNode["normal"] = vertex.normal;
-			vertexNode["colour"] = vertex.colour;
-			vertexNode["texPos"] = vertex.texPos;
-			vertexData.push_back(vertexNode);
-		}
+		YAML::Node modelNode;
+		for (auto& submesh : mesh.meshes) {
+			YAML::Node vertexData;
+			for (auto& vertex : submesh.data) {
+				YAML::Node vertexNode;
+				vertexNode["position"] = vertex.position;
+				vertexNode["normal"] = vertex.normal;
+				vertexNode["colour"] = vertex.colour;
+				vertexNode["texPos"] = vertex.texPos;
+				vertexData.push_back(vertexNode);
+			}
 
-		YAML::Node indexData;
-		for (auto& index : mesh.indexList()) {
-			indexData.push_back(index);
-		}
+			YAML::Node indexData;
+			for (auto& index : submesh.indices) {
+				indexData.push_back(index);
+			}
 
-		YAML::Node meshData;
-		meshData["vertex"] = vertexData;
-		meshData["index"] = indexData;
-		procMesh.set("mesh", meshData);
-		bunkerSpec.addComp(fggl::data::StaticMesh::guid, procMesh);
+			YAML::Node meshNode;
+			meshNode["vertex"] = vertexData;
+			meshNode["index"] = indexData;
+			modelNode.push_back( meshNode );
+		}
+		procMesh.set("model", modelNode);
+		bunkerSpec.addComp(fggl::mesh::StaticMultiMesh3D::guid, procMesh);
 
 		factory->define(BUNKER_PROTOTYPE, bunkerSpec);
 	}
diff --git a/demo/demo/models/viewer.cpp b/demo/demo/models/viewer.cpp
index c1860d2..bf7b3eb 100644
--- a/demo/demo/models/viewer.cpp
+++ b/demo/demo/models/viewer.cpp
@@ -28,6 +28,7 @@
 
 #include "fggl/gfx/phong.hpp"
 #include "fggl/gfx/camera.hpp"
+#include "fggl/gfx/paint.hpp"
 
 namespace demo {
 
@@ -90,6 +91,17 @@ namespace demo {
 		cameraKeys.rotate_ccw = glfwGetKeyScancode(GLFW_KEY_E);
 	}
 
+	static void setup_lighting(fggl::entity::EntityManager& ecs) {
+		auto light = ecs.create();
+		auto& transform = ecs.add<fggl::math::Transform>(light);
+
+		auto& lightComp = ecs.add<fggl::gfx::DirectionalLight>(light);
+		lightComp.position = fggl::math::vec3( 10.0F, 5.0F, 0.0F );
+		lightComp.diffuse = fggl::gfx::colours::CORNSILK;
+		lightComp.ambient = fggl::gfx::colours::MIDNIGHT_BLUE;
+		lightComp.specular = fggl::gfx::colours::MIDNIGHT_BLUE;
+	}
+
 	Viewer::Viewer(fggl::App &app) : fggl::scenes::Game(app), m_model(fggl::entity::INVALID) {
 
 	}
@@ -105,6 +117,7 @@ namespace demo {
 
 		// create camera
 		setup_camera(world());
+		setup_lighting(world());
 
 		// setup model
 		m_model = build_model(world(), manager);
diff --git a/demo/demo/topdown.cpp b/demo/demo/topdown.cpp
index c94ca17..0ad73c8 100644
--- a/demo/demo/topdown.cpp
+++ b/demo/demo/topdown.cpp
@@ -20,6 +20,8 @@
 
 #include "fggl/data/storage.hpp"
 #include "fggl/gfx/camera.hpp"
+#include "fggl/gfx/phong.hpp"
+
 #include "fggl/input/camera_input.hpp"
 #include "fggl/entity/loader/loader.hpp"
 
@@ -50,6 +52,17 @@ namespace demo {
 		cameraKeys.rotate_ccw = glfwGetKeyScancode(GLFW_KEY_E);
 	}
 
+	static void setup_lighting(fggl::entity::EntityManager& ecs) {
+		auto light = ecs.create();
+		auto& transform = ecs.add<fggl::math::Transform>(light);
+
+		auto& lightComp = ecs.add<fggl::gfx::DirectionalLight>(light);
+		lightComp.position = fggl::math::vec3( 10.0F, 5.0F, 0.0F );
+		lightComp.diffuse = fggl::gfx::colours::CORNSILK;
+		lightComp.ambient = fggl::gfx::colours::MIDNIGHT_BLUE;
+		lightComp.specular = fggl::gfx::colours::MIDNIGHT_BLUE;
+	}
+
 	static void place_cover_boxes(fggl::entity::EntityFactory* factory, fggl::entity::EntityManager& world) {
 		std::array<fggl::math::vec3,8> boxPos = {{
 													 {-10.0F, 0.0F, -10.0F},
@@ -91,6 +104,7 @@ namespace demo {
 			}
 		}
 
+		setup_lighting(world);
 		place_cover_boxes(factory, world);
 	}
 
diff --git a/fggl/data/assimp/module.cpp b/fggl/data/assimp/module.cpp
index bc587be..8ba7648 100644
--- a/fggl/data/assimp/module.cpp
+++ b/fggl/data/assimp/module.cpp
@@ -118,6 +118,16 @@ namespace fggl::data::models {
 			material->normalTextures.push_back(textureGuid);
 		}
 
+		for ( unsigned int i = 0U ; i < assimpMat->GetTextureCount(aiTextureType_SPECULAR); ++i ) {
+			aiString texName;
+			assimpMat->GetTexture( aiTextureType_SPECULAR, i, &texName );
+
+			auto textureGuid = prefix + "/" + texName.C_Str();
+			loader->load(textureGuid, DATA_TEXTURE2D, manager);
+
+			material->specularTextures.push_back(textureGuid);
+		}
+
 		manager->set( guid, material );
 	}
 
diff --git a/fggl/data/procedural.cpp b/fggl/data/procedural.cpp
index 34d9a2e..dfacbe9 100644
--- a/fggl/data/procedural.cpp
+++ b/fggl/data/procedural.cpp
@@ -23,6 +23,7 @@
 #include <array>
 
 #include <glm/geometric.hpp>
+#include "fggl/mesh/mesh.hpp"
 
 using namespace fggl::data;
 
@@ -33,48 +34,48 @@ static glm::vec3 calcSurfaceNormal(glm::vec3 vert1, glm::vec3 vert2, glm::vec3 v
 	return glm::normalize(glm::cross(edge1, edge2));
 }
 
-static void computeNormalsDirect(fggl::data::Mesh &mesh, const fggl::data::Mesh::IndexType *colIdx, int nPoints) {
+static void computeNormalsDirect(fggl::mesh::Mesh3D &mesh, const fggl::data::Mesh::IndexType *colIdx, int nPoints) {
 
 	// we're assuming all the normals are zero...
 	for (int i = 0; i < nPoints; i++) {
-		auto &vertex = mesh.vertex(colIdx[i]);
+		auto &vertex = mesh.data[colIdx[i]];
 		vertex.normal = glm::vec3(0.0F);
 	}
 
 	// We're assuming each vertex appears only once (because we're not indexed)
 	for (int i = 0; i < nPoints; i += 3) {
-		auto &v1 = mesh.vertex(colIdx[i]);
-		auto &v2 = mesh.vertex(colIdx[i + 1]);
-		auto &v3 = mesh.vertex(colIdx[i + 2]);
+		auto &v1 = mesh.data[colIdx[i]];
+		auto &v2 = mesh.data[colIdx[i + 1]];
+		auto &v3 = mesh.data[colIdx[i + 2]];
 
-		const glm::vec3 normal = glm::normalize(calcSurfaceNormal(v1.posititon, v2.posititon, v3.posititon));
+		const glm::vec3 normal = glm::normalize(calcSurfaceNormal(v1.position, v2.position, v3.position));
 		v1.normal = normal;
 		v2.normal = normal;
 		v3.normal = normal;
 	}
 }
 
-static void compute_normals(fggl::data::Mesh &mesh,
+static void compute_normals(fggl::mesh::Mesh3D &mesh,
 							const std::vector<Mesh::IndexType> &idxList, // source index
 							const std::vector<Mesh::IndexType> &idxMapping // source-to-mesh lookup
 ) {
 
 	// clear the normals, so the summation below works correctly
 	for (auto vertexIndex : idxMapping) {
-		auto &vertex = mesh.vertex(vertexIndex);
+		auto &vertex = mesh.data[vertexIndex];
 		vertex.normal = ILLEGAL_NORMAL;
 	}
 
 	// we need to calculate the contribution for each vertex
 	// this assumes IDXList describes a raw triangle list (ie, not quads and not a strip)
 	for (std::size_t i = 0; i < idxList.size(); i += 3) {
-		auto &v1 = mesh.vertex(idxMapping[idxList[i]]);
-		auto &v2 = mesh.vertex(idxMapping[idxList[i + 1]]);
-		auto &v3 = mesh.vertex(idxMapping[idxList[i + 2]]);
+		auto &v1 = mesh.data[ idxMapping[idxList[i]] ];
+		auto &v2 = mesh.data[ idxMapping[idxList[i + 1]] ];
+		auto &v3 = mesh.data[ idxMapping[idxList[i + 2]] ];
 
 		// calculate the normal and area (formula for area the math textbook)
-		float area = glm::length(glm::cross(v3.posititon - v2.posititon, v1.posititon - v3.posititon)) / 2;
-		auto faceNormal = calcSurfaceNormal(v1.posititon, v2.posititon, v3.posititon);
+		float area = glm::length(glm::cross(v3.position - v2.position, v1.position - v3.position)) / 2;
+		auto faceNormal = calcSurfaceNormal(v1.position, v2.position, v3.position);
 
 		// weight the normal according to the area of the surface (bigger area = more impact)
 		v1.normal += area * faceNormal;
@@ -84,12 +85,12 @@ static void compute_normals(fggl::data::Mesh &mesh,
 
 	// re-normalise the normals
 	for (unsigned int vertexIndex : idxMapping) {
-		auto &vertex = mesh.vertex(vertexIndex);
+		auto &vertex = mesh.data[vertexIndex];
 		vertex.normal = glm::normalize(vertex.normal);
 	}
 }
 
-static void populateMesh(fggl::data::Mesh &mesh, const fggl::math::mat4 transform,
+static void populateMesh(fggl::mesh::Mesh3D &mesh, const fggl::math::mat4 transform,
 						 const int nIdx, const fggl::math::vec3 *pos, const Mesh::IndexType *idx) {
 
 	auto *colIdx = new fggl::data::Mesh::IndexType[nIdx];
@@ -97,8 +98,8 @@ static void populateMesh(fggl::data::Mesh &mesh, const fggl::math::mat4 transfor
 	// generate mesh
 	for (int i = 0; i < nIdx; i++) {
 		glm::vec3 rawPos = transform * glm::vec4(pos[idx[i]], 1.0);
-		colIdx[i] = mesh.pushVertex(Vertex::from_pos(rawPos));
-		mesh.pushIndex(colIdx[i]);
+		colIdx[i] = mesh.append(fggl::mesh::Vertex3D::from_pos(rawPos));
+		mesh.indices.push_back(colIdx[i]);
 	}
 
 	computeNormalsDirect(mesh, colIdx, nIdx);
@@ -106,7 +107,7 @@ static void populateMesh(fggl::data::Mesh &mesh, const fggl::math::mat4 transfor
 	delete[] colIdx;
 }
 
-static void populateMesh(fggl::data::Mesh &mesh,
+static void populateMesh(fggl::mesh::Mesh3D &mesh,
 						 const fggl::math::mat4 transform,
 						 const std::vector<fggl::math::vec3> &posList,
 						 const std::vector<fggl::data::Mesh::IndexType> &idxList) {
@@ -118,12 +119,12 @@ static void populateMesh(fggl::data::Mesh &mesh,
 	// clion this thinks this loop is infinite, my assumption is it's gone bananas
 	for (std::size_t i = 0; i < posList.size(); ++i) {
 		glm::vec3 position = transform * fggl::math::vec4(posList[i], 1.0F);
-		colIdx[i] = mesh.pushVertex(Vertex::from_pos(position));
+		colIdx[i] = mesh.append(fggl::mesh::Vertex3D::from_pos(position));
 	}
 
 	// use the remapped indexes for the mesh
 	for (auto idx : idxList) {
-		mesh.pushIndex(colIdx[idx]);
+		mesh.indices.push_back(colIdx[idx]);
 	}
 
 	compute_normals(mesh, idxList, colIdx);
@@ -152,7 +153,7 @@ namespace fggl::data {
 		}
 	}
 
-	void make_sphere(Mesh &mesh, const math::mat4 &offset, uint32_t slices, uint32_t stacks) {
+	void make_sphere(fggl::mesh::Mesh3D &mesh, const math::mat4 &offset, uint32_t slices, uint32_t stacks) {
 
 		std::vector<math::vec3> positions;
 
@@ -254,7 +255,7 @@ fggl::data::Mesh fggl::data::make_quad_xz() {
 	return mesh;
 }
 
-fggl::data::Mesh fggl::data::make_cube(fggl::data::Mesh &mesh, const fggl::math::mat4 &transform) {
+fggl::mesh::Mesh3D fggl::data::make_cube(fggl::mesh::Mesh3D &mesh, const fggl::math::mat4 &transform) {
 	// done as two loops, top loop is 0,1,2,3, bottom loop is 4,5,6,7
 	constexpr std::array<fggl::math::vec3, 8> pos{{
 													  {-0.5, 0.5, -0.5}, // 0 TOP LOOP
@@ -285,7 +286,7 @@ fggl::data::Mesh fggl::data::make_cube(fggl::data::Mesh &mesh, const fggl::math:
 	return mesh;
 }
 
-fggl::data::Mesh fggl::data::make_slope(fggl::data::Mesh &mesh, const fggl::math::mat4 &transform) {
+fggl::mesh::Mesh3D fggl::data::make_slope(fggl::mesh::Mesh3D &mesh, const fggl::math::mat4 &transform) {
 
 	// done as two loops, top loop is 0,1,2,3, bottom loop is 4,5,6,7
 	// FIXME remove 2 and 3 and renumber the index list accordingly
@@ -316,7 +317,7 @@ fggl::data::Mesh fggl::data::make_slope(fggl::data::Mesh &mesh, const fggl::math
 	return mesh;
 }
 
-fggl::data::Mesh fggl::data::make_ditch(fggl::data::Mesh &mesh, const fggl::math::mat4 &transform) {
+fggl::mesh::Mesh3D fggl::data::make_ditch(fggl::mesh::Mesh3D &mesh, const fggl::math::mat4 &transform) {
 
 	// done as two loops, top loop is 0,1,2,3, bottom loop is 4,5,6,7
 	// FIXME remove 2 and renumber the index list accordingly
@@ -349,7 +350,7 @@ fggl::data::Mesh fggl::data::make_ditch(fggl::data::Mesh &mesh, const fggl::math
 	return mesh;
 }
 
-fggl::data::Mesh fggl::data::make_point(fggl::data::Mesh &mesh, const fggl::math::mat4 &transform) {
+fggl::mesh::Mesh3D fggl::data::make_point(fggl::mesh::Mesh3D &mesh, const fggl::math::mat4 &transform) {
 
 	// done as two loops, top loop is 0,1,2,3, bottom loop is 4,5,6,7
 	constexpr fggl::math::vec3 pos[]{
diff --git a/fggl/gfx/ogl/renderer.cpp b/fggl/gfx/ogl/renderer.cpp
index f0a4bb0..a2caa21 100644
--- a/fggl/gfx/ogl/renderer.cpp
+++ b/fggl/gfx/ogl/renderer.cpp
@@ -166,7 +166,7 @@ namespace fggl::gfx {
         	    //Set pixel to red
            		colours[ 0 ] = 0xFF;
             	colours[ 1 ] = 0x00;
-            	colours[ 2 ] = 0x00;
+            	colours[ 2 ] = 0xFF;
             	colours[ 3 ] = 0xFF;
         	}
 		}
diff --git a/fggl/gfx/ogl4/meshes.cpp b/fggl/gfx/ogl4/meshes.cpp
index 0a9dfb5..961c4f6 100644
--- a/fggl/gfx/ogl4/meshes.cpp
+++ b/fggl/gfx/ogl4/meshes.cpp
@@ -13,6 +13,8 @@
  */
 
 #include "fggl/gfx/ogl4/meshes.hpp"
+#include "fggl/gfx/phong.hpp"
+
 #include "fggl/data/texture.hpp"
 
 //
@@ -52,11 +54,22 @@ namespace fggl::gfx::ogl4 {
 		vao->bind();
 		vertexData->bind();
 
-		if ( material->m_diffuse != nullptr ) {
-			material->m_diffuse->bind(0);
-			if ( shader->hasUniform("diffuseTexture") ) {
-				shader->setUniformI(shader->uniform("diffuseTexture"), 0);
+		if ( material != nullptr ) {
+			if (material->m_diffuse != nullptr) {
+				material->m_diffuse->bind(0);
+				if (shader->hasUniform("diffuseTexture")) {
+					shader->setUniformI(shader->uniform("diffuseTexture"), 0);
+				}
+			}
+
+			if (material->m_specular != nullptr) {
+				material->m_specular->bind(1);
+				if (shader->hasUniform("specularTexture")) {
+					shader->setUniformI(shader->uniform("specularTexture"), 1);
+				}
 			}
+		} else {
+			debug::info("no material is active, cannot bind textures!");
 		}
 
 		if ( drawInfo.restartIndex != ogl::NO_RESTART_IDX) {
@@ -90,6 +103,19 @@ namespace fggl::gfx::ogl4 {
 		shader->setUniformF(shader->uniform("materials[0].shininess"), material->shininess);
 	}
 
+	void setup_lighting(const std::shared_ptr<ogl::Shader>& shader, const DirectionalLight* light) {
+		assert( light != nullptr );
+		if ( !shader->hasUniform("light.direction") ) {
+			fggl::debug::warning("asked for directional lighting, but shader does not support!");
+			return;
+		}
+
+		shader->setUniformF( shader->uniform("light.direction"), light->position);
+		shader->setUniformF( shader->uniform("light.ambient"), light->ambient);
+		shader->setUniformF( shader->uniform("light.diffuse"), light->diffuse);
+		shader->setUniformF( shader->uniform("light.specular"), light->specular);
+	}
+
 	void setup_lighting(const std::shared_ptr<ogl::Shader>& shader, const math::mat4& viewMatrix, const math::Transform& camTransform, const math::Transform& transform, math::vec3 lightPos) {
 		if (shader->hasUniform("lightPos")) {
 			shader->setUniformF(shader->uniform("lightPos"), lightPos);
@@ -132,72 +158,6 @@ namespace fggl::gfx::ogl4 {
 		shader->setUniformF(shader->uniform("lights[0].colour"), {0.5f, 0.5f, 0.5f});
 	}
 
-
-	void forward_pass_multi_mesh(const entity::EntityID& camera, const fggl::entity::EntityManager& world) {
-
-		// 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());
-
-		// TODO lighting needs to not be this...
-		math::vec3 lightPos{0.0f, 10.0f, 0.0f};
-
-		std::shared_ptr<ogl::Shader> shader = nullptr;
-		ogl::Location mvpMatrixUniform = 0;
-		ogl::Location mvMatrixUniform = 0;
-
-		auto entityView = world.find<StaticMultiMesh>();
-		for (const auto &entity : entityView) {
-			debug::trace("Multi-mesh happened!");
-
-			// ensure that the model pipeline actually exists...
-			const auto &model = world.get<StaticMultiMesh>(entity);
-			if (model.pipeline == nullptr) {
-				debug::warning("shader was null, aborting render");
-				continue;
-			}
-
-			// check if we switched shaders
-			if (shader == nullptr || shader->shaderID() != model.pipeline->shaderID()) {
-				// new shader - need to re-send the view and projection matrices
-				shader = model.pipeline;
-				shader->use();
-				if (shader->hasUniform("projection")) {
-					shader->setUniformMtx(shader->uniform("view"), viewMatrix);
-					shader->setUniformMtx(shader->uniform("projection"), projectionMatrix);
-				}
-				mvpMatrixUniform = shader->uniform("MVPMatrix");
-				mvMatrixUniform = shader->uniform("MVMatrix");
-			}
-
-			// set model transform
-			const auto &transform = world.get<math::Transform>(entity);
-			shader->setUniformMtx(mvpMatrixUniform, projectionMatrix * viewMatrix * transform.model());
-			shader->setUniformMtx(mvMatrixUniform, viewMatrix * transform.model());
-
-			auto normalMatrix = glm::mat3(glm::transpose(inverse(transform.model())));
-			shader->setUniformMtx(shader->uniform("NormalMatrix"), normalMatrix);
-
-			// setup lighting mode
-			setup_lighting(shader, viewMatrix, camTransform, transform, lightPos);
-			setup_material(shader, world.tryGet<PhongMaterial>(entity, &DEFAULT_MATERIAL));
-
-			model.draw();
-
-		}
-	}
-
 	static ogl::Texture* upload_texture( std::string name, assets::AssetManager* manager ) {
 		debug::info("loading texture: {}", name);
 
@@ -220,11 +180,24 @@ namespace fggl::gfx::ogl4 {
 		return manager->set("ogl_"+name, texture);
 	}
 
+	static Material* get_fallback_material(assets::AssetManager* manager) {
+		auto* fallback = manager->get<Material>(FALLBACK_MAT);
+		if ( fallback != nullptr ) {
+			return fallback;
+		}
+
+		Material* mat = new Material();
+		mat->m_diffuse = manager->get<ogl::Texture>(FALLBACK_TEX);
+		mat->m_specular = manager->get<ogl::Texture>(FALLBACK_TEX);
+		mat->m_normals = manager->get<ogl::Texture>(FALLBACK_TEX);
+		return manager->set(FALLBACK_MAT, mat);
+	}
+
 	static Material* upload_material( std::string name, assets::AssetManager* manager ) {
 		auto* meshMaterial = manager->get<mesh::Material>(name);
 		if ( meshMaterial == nullptr ){
 			debug::error("attempted to load material {}, but did not exist!", name);
-			return {};
+			return get_fallback_material(manager);
 		}
 
 		auto* material = manager->get<Material>("ogl_"+name);
@@ -239,6 +212,9 @@ namespace fggl::gfx::ogl4 {
 		if ( !meshMaterial->normalTextures.empty() ) {
 			material->m_normals = upload_texture(meshMaterial->getPrimaryNormals(), manager);
 		}
+		if ( !meshMaterial->specularTextures.empty() ) {
+			material->m_specular = upload_texture(meshMaterial->getPrimarySpecular(), manager);
+		}
 
 		return manager->set("ogl_"+name, material);
 	}
diff --git a/fggl/gfx/ogl4/models.cpp b/fggl/gfx/ogl4/models.cpp
index 8bfdce4..d2c61b7 100644
--- a/fggl/gfx/ogl4/models.cpp
+++ b/fggl/gfx/ogl4/models.cpp
@@ -428,7 +428,7 @@ namespace fggl::gfx::ogl4 {
 		// perform a rendering pass for each camera (will usually only be one...)
 		for (const auto &cameraEnt : cameras) {
 			//TODO should be clipping this to only visible objects
-			forward_camera_pass(cameraEnt, world);
+			//forward_camera_pass(cameraEnt, world);
 
 			forward_pass<ogl4::StaticMesh>(cameraEnt, world, m_assets);
 			forward_pass<ogl4::StaticMultiMesh>(cameraEnt, world, m_assets);
diff --git a/fggl/gfx/ogl4/module.cpp b/fggl/gfx/ogl4/module.cpp
index e4bb673..b20b384 100644
--- a/fggl/gfx/ogl4/module.cpp
+++ b/fggl/gfx/ogl4/module.cpp
@@ -31,7 +31,7 @@ namespace fggl::gfx {
 	constexpr const char *SHAPE_SPHERE{"sphere"};
 	constexpr const char *SHAPE_BOX{"box"};
 
-	static void process_shape(const YAML::Node &node, data::Mesh &mesh) {
+	static void process_shape(const YAML::Node &node, mesh::Mesh3D &mesh) {
 		auto transform = data::OFFSET_NONE;
 
 		auto offset = node["offset"].as<math::vec3>(math::VEC3_ZERO);
@@ -66,46 +66,57 @@ namespace fggl::gfx {
 
 		// asset is a procedural mesh description
 		if (spec.has("shape")) {
-			data::Mesh* meshAsset = nullptr;
 			auto pipeline = spec.get<std::string>("pipeline", "");
 
 			// check if we had previously loaded this asset
-			const auto shapeName = spec.get<std::string>("shape_id", "");
+			/*const auto shapeName = spec.get<std::string>("shape_id", "");
 			if ( !shapeName.empty() ) {
 				meshAsset = assetService->get<data::Mesh>(shapeName);
-			}
+			}*/
 
 			// we've not loaded this before - generate mesh
-			if ( meshAsset == nullptr ) {
-				// procedural meshes are build as static meshes first
-				auto* meshTmp = new data::Mesh();
+			if ( true ) {
 
+				// procedural meshes are build as static meshes first
 				if (spec.config["shape"].IsSequence()) {
+					mesh::MultiMesh3D* multiMesh;
+
 					for (const auto &node : spec.config["shape"]) {
-						process_shape(node, *meshTmp);
+						mesh::Mesh3D* meshAsset = new mesh::Mesh3D();
+						process_shape(node, *meshAsset);
+						multiMesh->meshes.push_back(*meshAsset);
+						delete meshAsset;
 					}
-				} else {
-					process_shape(spec.config["shape"], *meshTmp);
-				}
-				meshTmp->removeDups();
 
-				if (!shapeName.empty()) {
-					meshAsset = assetService->set(shapeName, meshTmp);
+					#ifdef FGGL_ALLOW_DEFERRED_UPLOAD
+						// the graphics stack can detect static meshes without a rendering proxy at runtime and fix it but this
+						// requires loading the whole model into the ECS (and should be removed in the future). instead we should
+						// be triggering the upload at this point (if needed).
+						auto &entityMesh = manager.add<mesh::StaticMultiMesh3D>(id);
+						entityMesh.mesh = *multiMesh;
+						entityMesh.pipeline = pipeline;
+						debug::warning("HACKY: Triggered proc mesh - using deferred upload");
+					#endif
+
 				} else {
-					meshAsset = meshTmp;
+					mesh::Mesh3D* meshAsset = new mesh::Mesh3D();
+					process_shape(spec.config["shape"], *meshAsset);
+
+					#ifdef FGGL_ALLOW_DEFERRED_UPLOAD
+						// the graphics stack can detect static meshes without a rendering proxy at runtime and fix it but this
+						// requires loading the whole model into the ECS (and should be removed in the future). instead we should
+						// be triggering the upload at this point (if needed).
+						auto &entityMesh = manager.add<mesh::StaticMesh3D>(id);
+						entityMesh.mesh = *meshAsset;
+						entityMesh.pipeline = pipeline;
+						debug::warning("HACKY: Triggered proc mesh - using deferred upload");
+					#endif
 				}
+
+				//assetService->set(shapeName, meshTmp);
 			}
 
 			// TODO we need to trigger an upload to the GPU (somehow)
-			#ifdef FGGL_ALLOW_DEFERRED_UPLOAD
-				// the graphics stack can detect static meshes without a rendering proxy at runtime and fix it but this
-				// requires loading the whole model into the ECS (and should be removed in the future). instead we should
-				// be triggering the upload at this point (if needed).
-				auto &entityMesh = manager.add<data::StaticMesh>(id);
-				entityMesh.pipeline = pipeline;
-				entityMesh.mesh = *meshAsset;
-				debug::warning("HACKY: Triggered proc mesh - using deferred upload");
-			#endif
 
 			return;
 		}
@@ -139,8 +150,21 @@ namespace fggl::gfx {
 		mat.shininess = spec.get<float>("ambient", gfx::DEFAULT_SHININESS);
 	}
 
-	void attach_light(const entity::ComponentSpec &spec, entity::EntityManager &manager, const entity::EntityID &id, modules::Services& services) {
-		auto &light = manager.add<gfx::Light>(id);
+	void attach_light_directional(const entity::ComponentSpec &spec, entity::EntityManager &manager, const entity::EntityID &id, modules::Services& services) {
+		auto &light = manager.add<gfx::DirectionalLight>(id);
+		light.position = spec.get<math::vec3>("direction", -math::UP);
+		light.ambient = spec.get<math::vec3>("ambient", gfx::colours::WHITE);
+		light.specular = spec.get<math::vec3>("specular", gfx::colours::WHITE);
+		light.diffuse = spec.get<math::vec3>("diffuse", gfx::colours::WHITE);
+	}
+
+	void attach_light_point(const entity::ComponentSpec &spec, entity::EntityManager &manager, const entity::EntityID &id, modules::Services& services) {
+		auto &light = manager.add<gfx::PointLight>(id);
+		light.position = spec.get<math::vec3>("position", math::VEC3_ZERO);
+
+		light.constant = spec.get<float>("constant", 1.0F);
+		light.linear = spec.get<float>("linear", 0.0014F);
+		light.quadratic = spec.get<float>("quadratic", 0.000007F);
 	}
 
 	bool OpenGL4::factory(modules::ModuleService service, modules::Services &services) {
@@ -158,7 +182,9 @@ namespace fggl::gfx {
 			entityFactory->bind(mesh::StaticMesh3D::guid, attach_mesh);
 			entityFactory->bind(mesh::StaticMultiMesh3D::guid, attach_mesh);
 			entityFactory->bind(gfx::PhongMaterial::guid, attach_material);
-			entityFactory->bind(gfx::Light::guid, attach_light);
+
+			entityFactory->bind(gfx::DirectionalLight::guid, attach_light_directional);
+			entityFactory->bind(gfx::PointLight::guid, attach_light_point);
 
 			return true;
 		}
diff --git a/fggl/scenes/game.cpp b/fggl/scenes/game.cpp
index 14a2139..bf104d2 100644
--- a/fggl/scenes/game.cpp
+++ b/fggl/scenes/game.cpp
@@ -67,18 +67,23 @@ namespace fggl::scenes {
 			if (escapePressed) {
 				m_owner.change_state(m_previous);
 			}
+
+			if ( m_input->keyboard.pressed(glfwGetKeyScancode(GLFW_KEY_F2)) ) {
+				m_debug = !m_debug;
+			}
 		}
 
 		if (m_phys != nullptr) {
 			m_phys->step();
 		}
 
+		// debug render toggle
 		//m_world->reapEntities();
 	}
 
 	void Game::render(fggl::gfx::Graphics &gfx) {
 		if (m_world != nullptr) {
-			gfx.drawScene(*m_world);
+			gfx.drawScene(*m_world, m_debug);
 		}
 	}
 
diff --git a/include/fggl/data/procedural.hpp b/include/fggl/data/procedural.hpp
index 53ec3e6..b4471b3 100644
--- a/include/fggl/data/procedural.hpp
+++ b/include/fggl/data/procedural.hpp
@@ -16,6 +16,7 @@
 #define FGGL_DATA_PROCEDURAL_HPP
 
 #include "model.hpp"
+#include "fggl/mesh/mesh.hpp"
 
 namespace fggl::data {
 
@@ -28,7 +29,7 @@ namespace fggl::data {
 
 	// platonic solids
 	void make_tetrahedron(Mesh &mesh, const math::mat4 &offset = OFFSET_NONE);
-	Mesh make_cube(Mesh &mesh, const math::mat4 &offset = OFFSET_NONE);
+	mesh::Mesh3D make_cube(mesh::Mesh3D &mesh, const math::mat4 &offset = OFFSET_NONE);
 	void make_octahedron(Mesh &mesh, const math::mat4 &offset = OFFSET_NONE);
 	void make_icosahedron(Mesh &mesh, const math::mat4 &offset = OFFSET_NONE);
 	void make_dodecahedron(Mesh &mesh, const math::mat4 &offset = OFFSET_NONE);
@@ -38,13 +39,13 @@ namespace fggl::data {
 	void make_sphere_iso(Mesh &mesh, const math::mat4 &offset = OFFSET_NONE);
 
 	// level block-out shapes
-	Mesh make_slope(Mesh &mesh, const math::mat4 &offset = OFFSET_NONE);
-	Mesh make_ditch(Mesh &mesh, const math::mat4 &offset = OFFSET_NONE);
-	Mesh make_point(Mesh &mesh, const math::mat4 &offset = OFFSET_NONE);
+	mesh::Mesh3D make_slope(mesh::Mesh3D &mesh, const math::mat4 &offset = OFFSET_NONE);
+	mesh::Mesh3D make_ditch(mesh::Mesh3D &mesh, const math::mat4 &offset = OFFSET_NONE);
+	mesh::Mesh3D make_point(mesh::Mesh3D &mesh, const math::mat4 &offset = OFFSET_NONE);
 
 	// other useful types people expect
 	void make_capsule(Mesh &mesh);
-	void make_sphere(Mesh &mesh, const math::mat4 &offset = OFFSET_NONE, uint32_t stacks = 16U, uint32_t slices = 16U);
+	void make_sphere(mesh::Mesh3D &mesh, const math::mat4 &offset = OFFSET_NONE, uint32_t stacks = 16U, uint32_t slices = 16U);
 }
 
 #endif
\ No newline at end of file
diff --git a/include/fggl/gfx/ogl4/fallback.hpp b/include/fggl/gfx/ogl4/fallback.hpp
index 3d725c5..27479b9 100644
--- a/include/fggl/gfx/ogl4/fallback.hpp
+++ b/include/fggl/gfx/ogl4/fallback.hpp
@@ -61,6 +61,7 @@ namespace fggl::gfx::ogl4 {
 	constexpr const std::array<uint32_t, 4> TEX_WHITE{ 0xFF, 0xFF, 0xFF, 0xFF };
 	constexpr const std::array<uint32_t, 4> TEX_CHECKER{ 0xFF, 0x00, 0x00, 0xFF };
 	constexpr const char* FALLBACK_TEX = "FALLBACK_TEX";
+	constexpr const char* FALLBACK_MAT = "FALLBACK_MAT";
 
 } // namespace fggl::gfx::ogl4
 
diff --git a/include/fggl/gfx/ogl4/meshes.hpp b/include/fggl/gfx/ogl4/meshes.hpp
index 196ed78..7cae65b 100644
--- a/include/fggl/gfx/ogl4/meshes.hpp
+++ b/include/fggl/gfx/ogl4/meshes.hpp
@@ -39,6 +39,7 @@ namespace fggl::gfx::ogl4 {
 	struct Material {
 		ogl::Texture* m_diffuse;
 		ogl::Texture* m_normals;
+		ogl::Texture* m_specular;
 	};
 
 	struct MeshData {
@@ -76,6 +77,8 @@ namespace fggl::gfx::ogl4 {
 	void setup_material(const std::shared_ptr<ogl::Shader>& shader, const PhongMaterial* material);
 	void setup_lighting(const std::shared_ptr<ogl::Shader>& shader, const math::mat4& viewMatrix, const math::Transform& camTransform, const math::Transform& transform, math::vec3 lightPos);
 
+	void setup_lighting(const std::shared_ptr<ogl::Shader>& shader, const DirectionalLight* light);
+
 	template<typename T>
 	void forward_pass(const entity::EntityID& camera, const fggl::entity::EntityManager& world, const assets::AssetManager* assets) {
 
@@ -89,6 +92,7 @@ namespace fggl::gfx::ogl4 {
 		// prep the fallback textures
 		auto *fallbackTex = assets->template get<ogl::Texture>(FALLBACK_TEX);
 		fallbackTex->bind(0);
+		fallbackTex->bind(1);
 
 		// set-up camera matrices
 		const auto &camTransform = world.get<fggl::math::Transform>(camera);
@@ -97,9 +101,6 @@ namespace fggl::gfx::ogl4 {
 		const math::mat4 projectionMatrix = camComp.perspective();
 		const math::mat4 viewMatrix = glm::lookAt(camTransform.origin(), camComp.target, camTransform.up());
 
-		// TODO lighting needs to not be this...
-		math::vec3 lightPos{0.0F, 10.0F, 0.0F};
-
 		std::shared_ptr<ogl::Shader> shader = nullptr;
 		ogl::Location mvpMatrixUniform = 0;
 		ogl::Location mvMatrixUniform = 0;
@@ -107,6 +108,16 @@ namespace fggl::gfx::ogl4 {
 		auto entityView = world.find<T>();
 		debug::info("Triggering rendering pass for {} entities", entityView.size());
 
+		// find directional light in scene
+		auto lightEnts = world.find<DirectionalLight>();
+		const DirectionalLight* light;
+		if ( !lightEnts.empty() ) {
+			light = world.tryGet<DirectionalLight>(lightEnts[0]);
+		} else {
+			debug::warning("no light component in scene, it's gunna be dark...");
+			light = nullptr;
+		}
+
 		for (const auto &entity : entityView) {
 			// ensure that the model pipeline actually exists...
 			const auto &model = world.get<T>(entity);
@@ -130,6 +141,10 @@ namespace fggl::gfx::ogl4 {
 				if ( shader->hasUniform("diffuseTexture") ) {
 					shader->setUniformI(shader->uniform("diffuseTexture"), 0);
 				}
+
+				if ( shader->hasUniform("specularTexture") ) {
+					shader->setUniformI(shader->uniform("specularTexture"), 1);
+				}
 			}
 
 			// set model transform
@@ -137,11 +152,14 @@ namespace fggl::gfx::ogl4 {
 			shader->setUniformMtx(mvpMatrixUniform, projectionMatrix * viewMatrix * transform.model());
 			shader->setUniformMtx(mvMatrixUniform, viewMatrix * transform.model());
 
-			auto normalMatrix = glm::mat3(glm::transpose(inverse(transform.model())));
-			shader->setUniformMtx(shader->uniform("NormalMatrix"), normalMatrix);
+			/*auto normalMatrix = glm::mat3(glm::transpose(inverse(transform.model())));
+			shader->setUniformMtx(shader->uniform("NormalMatrix"), normalMatrix);*/
 
 			// setup lighting mode
-			setup_lighting(shader, viewMatrix, camTransform, transform, lightPos);
+			//setup_lighting(shader, viewMatrix, camTransform, transform, lightPos);
+			if ( light != nullptr ) {
+				setup_lighting(shader, light);
+			}
 			setup_material(shader, world.tryGet<PhongMaterial>(entity, &DEFAULT_MATERIAL));
 
 			model.draw();
diff --git a/include/fggl/gfx/ogl4/models.hpp b/include/fggl/gfx/ogl4/models.hpp
index 66ffbe2..5b8fba6 100644
--- a/include/fggl/gfx/ogl4/models.hpp
+++ b/include/fggl/gfx/ogl4/models.hpp
@@ -71,6 +71,7 @@ namespace fggl::gfx::ogl4 {
 		public:
 			inline StaticModelRenderer(gfx::ShaderCache *cache, assets::AssetManager *assets)
 				: m_assets(assets), m_shaders(cache), m_phong(nullptr), m_vao(), m_vertexList(), m_indexList() {
+
 				m_phong = cache->get("redbook/debug");
 			}
 
diff --git a/include/fggl/gfx/phong.hpp b/include/fggl/gfx/phong.hpp
index e0852ee..5bd3a31 100644
--- a/include/fggl/gfx/phong.hpp
+++ b/include/fggl/gfx/phong.hpp
@@ -50,6 +50,27 @@ namespace fggl::gfx {
 	};
 
 	struct Light {
+		math::vec3 position;
+		math::vec3 ambient;
+		math::vec3 specular;
+		math::vec3 diffuse;
+	};
+
+	struct DirectionalLight : public Light {
+		constexpr static const char *name = "gfx::phong::directional";
+		constexpr static const util::GUID guid = util::make_guid(name);
+	};
+
+	struct PointLight : public Light {
+		constexpr static const char *name = "gfx::phong::point";
+		constexpr static const util::GUID guid = util::make_guid(name);
+
+		float constant = 1.0F;
+		float linear;
+		float quadratic;
+	};
+
+	struct Light2 {
 		constexpr static const char *name = "gfx::light";
 		constexpr static const util::GUID guid = util::make_guid("gfx::light");
 		bool enabled;
diff --git a/include/fggl/mesh/mesh.hpp b/include/fggl/mesh/mesh.hpp
index 005ce8a..1da8896 100644
--- a/include/fggl/mesh/mesh.hpp
+++ b/include/fggl/mesh/mesh.hpp
@@ -29,8 +29,17 @@ namespace fggl::mesh {
 	struct Vertex3D {
 		math::vec3 position;
 		math::vec3 normal;
-		math::vec3 colour{ 1.0F, 1.0F, 1.0f };
+		math::vec3 colour{ 1.0F, 1.0F, 1.0F };
 		math::vec2 texPos{ NAN, NAN };
+
+		static Vertex3D from_pos(math::vec3 pos) {
+			return {
+				.position = pos,
+				.normal {NAN, NAN, NAN},
+				.colour {1.0F, 1.0F, 1.0F},
+				.texPos { pos.x, pos.z }
+			};
+		}
 	};
 
 	struct Vertex2D {
@@ -50,6 +59,7 @@ namespace fggl::mesh {
 		math::vec3 specular;
 		std::vector<std::string> diffuseTextures{};
 		std::vector<std::string> normalTextures{};
+		std::vector<std::string> specularTextures{};
 
 		inline std::string getPrimaryDiffuse() {
 			assert( !diffuseTextures.empty() );
@@ -60,6 +70,11 @@ namespace fggl::mesh {
 			assert( !normalTextures.empty() );
 			return normalTextures.empty() ? "" : normalTextures[0];
 		}
+
+		inline std::string getPrimarySpecular() {
+			assert( !specularTextures.empty() );
+			return specularTextures.empty() ? "" : specularTextures[0];
+		}
 	};
 
 	template<typename VertexFormat>
@@ -67,12 +82,22 @@ namespace fggl::mesh {
 		std::vector<VertexFormat> data;
 		std::vector<uint32_t> indices;
 		std::string material;
+
+		inline uint32_t append(const VertexFormat& vert) {
+			auto nextIdx = data.size();
+			data.push_back(vert);
+			return nextIdx;
+		}
 	};
 
 	template<typename MeshFormat>
 	struct MultiMesh {
 		std::vector<MeshFormat> meshes;
 		std::vector<std::string> materials;
+
+		MeshFormat& generate() {
+			return meshes.template emplace_back();
+		}
 	};
 
 	using Mesh2D = Mesh<Vertex2D>;
diff --git a/include/fggl/scenes/game.hpp b/include/fggl/scenes/game.hpp
index 21f35cf..d844f7f 100644
--- a/include/fggl/scenes/game.hpp
+++ b/include/fggl/scenes/game.hpp
@@ -77,6 +77,8 @@ namespace fggl::scenes {
 				return *m_input;
 			}
 
+			bool m_debug;
+
 		private:
 			input::Input *m_input;
 			std::unique_ptr<entity::EntityManager> m_world;
diff --git a/vendor/imgui/CMakeLists.txt b/vendor/imgui/CMakeLists.txt
index 665c472..6353234 100644
--- a/vendor/imgui/CMakeLists.txt
+++ b/vendor/imgui/CMakeLists.txt
@@ -23,5 +23,5 @@ include(GNUInstallDirs)
 install(TARGETS imgui
 		EXPORT fgglTargets
 		PUBLIC_HEADER
-		  DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/fggl/imgui
+		DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/fggl/imgui
 )
\ No newline at end of file
-- 
GitLab