Browse Source

Merge branch 'graphics_import'

* graphics_import: (26 commits)
  Cleanup
  Don't use a member function.
  Add ability to convert a graphics object to a vertex list
  Don't provide the renderer impl through the constructor of direct_renderer.
  Add draw function to opengl_renderer. Move vertex to own file.
  Start writing openGL renderer implementation.
  Start adding renderer code. - Define a batch renderer to minimize material-context switching. - Move hash implementations to their own file in the gameutils toolkit.
  Add activate material code.
  Fix hashing for material
  Making flyweight bindings more intelligent.
  Start writing code for material.
  Make things public. Add error printing for shader_program errors.
  Add data members to texture/shader/shader_program. Use flyweight<T> to get quick-use versions.
  Refactor out flyweight object. TODO: Move members out to the object.
  Make things a little easier.
  Activate shader programs
  Add code for constructing a shader_program, rename graphics::program to graphics::shader_program. TODO: Activate shader_program TODO: Verbose linker errors.
  Add the three default textures.
  Adding error printers for shader.
  Rename env.cxx to linux_env.cxx. Add osx_env.mm for loading from macOS.
  ...
Sam Jaffe 6 years ago
parent
commit
31acbfa56d

+ 2 - 2
engine/engine.xcodeproj/project.pbxproj

@@ -454,7 +454,7 @@
 				MTL_ENABLE_DEBUG_INFO = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = macosx;
-				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include/ $(PROJECT_DIR)/../include/expect/include $(PROJECT_DIR)/../ $(PROJECT_DIR)/../math/include/ $(PROJECT_DIR)/../math/";
+				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include/ $(PROJECT_DIR)/../include/expect/include $(PROJECT_DIR)/../ $(PROJECT_DIR)/../graphics/include/ $(PROJECT_DIR)/../math/include/ $(PROJECT_DIR)/../util/include/ $(PROJECT_DIR)/../math/";
 			};
 			name = Debug;
 		};
@@ -500,7 +500,7 @@
 				MACOSX_DEPLOYMENT_TARGET = 10.10;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				SDKROOT = macosx;
-				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include/ $(PROJECT_DIR)/../include/expect/include $(PROJECT_DIR)/../ $(PROJECT_DIR)/../math/include/ $(PROJECT_DIR)/../math/";
+				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include/ $(PROJECT_DIR)/../include/expect/include $(PROJECT_DIR)/../ $(PROJECT_DIR)/../graphics/include/ $(PROJECT_DIR)/../math/include/ $(PROJECT_DIR)/../util/include/ $(PROJECT_DIR)/../math/";
 			};
 			name = Release;
 		};

+ 2 - 1
engine/include/game/engine/scene.hpp

@@ -8,9 +8,10 @@
 #pragma once
 
 #include <memory>
+#include <vector>
 
 #include "game/math/math_fwd.hpp"
-#include "util/identity.hpp"
+#include "game/util/identity.hpp"
 #include "vector/vector.hpp"
 
 #include "engine_fwd.hpp"

+ 51 - 7
graphics/graphics.xcodeproj/project.pbxproj

@@ -12,6 +12,13 @@
 		CD3AC6FD1D2C06B5002B4BB0 /* shader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CD3AC6FB1D2C06B5002B4BB0 /* shader.cpp */; };
 		CD3AC7191D2C0950002B4BB0 /* shader_program.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CD3AC7171D2C0950002B4BB0 /* shader_program.cpp */; };
 		CD3AC7261D2C0C63002B4BB0 /* object.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CD3AC7241D2C0C63002B4BB0 /* object.cpp */; };
+		CD62FCF72290DC9000376440 /* helper.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CD62FCF52290DC9000376440 /* helper.hpp */; };
+		CD62FCF82290DC9000376440 /* opengl_helper.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD62FCF62290DC9000376440 /* opengl_helper.cxx */; };
+		CD62FCFA2290E2E500376440 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD62FCF92290E2E500376440 /* OpenGL.framework */; };
+		CD62FD062291970F00376440 /* libgameutils.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD62FD052291970F00376440 /* libgameutils.dylib */; };
+		CD62FD1E2292412900376440 /* renderer.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD62FD1C2292412900376440 /* renderer.cxx */; };
+		CD62FD222292C76B00376440 /* renderer_impl.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CD62FD202292C76B00376440 /* renderer_impl.hpp */; };
+		CD62FD232292C76B00376440 /* opengl_renderer.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD62FD212292C76B00376440 /* opengl_renderer.cxx */; };
 		CDA34D9A22517A3D008036A7 /* libmath.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CDA34D9922517A3D008036A7 /* libmath.dylib */; };
 /* End PBXBuildFile section */
 
@@ -54,6 +61,13 @@
 		CD3AC7171D2C0950002B4BB0 /* shader_program.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = shader_program.cpp; sourceTree = "<group>"; };
 		CD3AC7241D2C0C63002B4BB0 /* object.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = object.cpp; sourceTree = "<group>"; };
 		CD62FCD722904AD100376440 /* GoogleMock.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GoogleMock.xcodeproj; path = "../../gmock-xcode-master/GoogleMock.xcodeproj"; sourceTree = "<group>"; };
+		CD62FCF52290DC9000376440 /* helper.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = helper.hpp; sourceTree = "<group>"; };
+		CD62FCF62290DC9000376440 /* opengl_helper.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = opengl_helper.cxx; sourceTree = "<group>"; };
+		CD62FCF92290E2E500376440 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; };
+		CD62FD052291970F00376440 /* libgameutils.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; path = libgameutils.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
+		CD62FD1C2292412900376440 /* renderer.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = renderer.cxx; sourceTree = "<group>"; };
+		CD62FD202292C76B00376440 /* renderer_impl.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = renderer_impl.hpp; sourceTree = "<group>"; };
+		CD62FD212292C76B00376440 /* opengl_renderer.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = opengl_renderer.cxx; sourceTree = "<group>"; };
 		CDA34D86225171AA008036A7 /* game */ = {isa = PBXFileReference; lastKnownFileType = folder; name = game; path = include/game; sourceTree = "<group>"; };
 		CDA34D9922517A3D008036A7 /* libmath.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; path = libmath.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
 /* End PBXFileReference section */
@@ -63,6 +77,8 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CD62FD062291970F00376440 /* libgameutils.dylib in Frameworks */,
+				CD62FCFA2290E2E500376440 /* OpenGL.framework in Frameworks */,
 				CDA34D9A22517A3D008036A7 /* libmath.dylib in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -92,11 +108,10 @@
 		CD3AC6E41D2C0364002B4BB0 /* src */ = {
 			isa = PBXGroup;
 			children = (
-				CD3AC6F01D2C03B7002B4BB0 /* material.cpp */,
-				CD3AC6FB1D2C06B5002B4BB0 /* shader.cpp */,
-				CD3AC7171D2C0950002B4BB0 /* shader_program.cpp */,
-				CD3AC6F61D2C0518002B4BB0 /* texture.cpp */,
-				CD3AC7241D2C0C63002B4BB0 /* object.cpp */,
+				CD62FD1A22923B8E00376440 /* renderer */,
+				CD62FD1822923B8100376440 /* model */,
+				CD62FCF52290DC9000376440 /* helper.hpp */,
+				CD62FCF62290DC9000376440 /* opengl_helper.cxx */,
 			);
 			path = src;
 			sourceTree = "<group>";
@@ -112,9 +127,33 @@
 			name = Products;
 			sourceTree = "<group>";
 		};
+		CD62FD1822923B8100376440 /* model */ = {
+			isa = PBXGroup;
+			children = (
+				CD3AC7241D2C0C63002B4BB0 /* object.cpp */,
+				CD3AC6F01D2C03B7002B4BB0 /* material.cpp */,
+				CD3AC6FB1D2C06B5002B4BB0 /* shader.cpp */,
+				CD3AC7171D2C0950002B4BB0 /* shader_program.cpp */,
+				CD3AC6F61D2C0518002B4BB0 /* texture.cpp */,
+			);
+			name = model;
+			sourceTree = "<group>";
+		};
+		CD62FD1A22923B8E00376440 /* renderer */ = {
+			isa = PBXGroup;
+			children = (
+				CD62FD1C2292412900376440 /* renderer.cxx */,
+				CD62FD202292C76B00376440 /* renderer_impl.hpp */,
+				CD62FD212292C76B00376440 /* opengl_renderer.cxx */,
+			);
+			name = renderer;
+			sourceTree = "<group>";
+		};
 		CDA34D9822517A3D008036A7 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				CD62FD052291970F00376440 /* libgameutils.dylib */,
+				CD62FCF92290E2E500376440 /* OpenGL.framework */,
 				CDA34D9922517A3D008036A7 /* libmath.dylib */,
 			);
 			name = Frameworks;
@@ -127,6 +166,8 @@
 			isa = PBXHeadersBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CD62FD222292C76B00376440 /* renderer_impl.hpp in Headers */,
+				CD62FCF72290DC9000376440 /* helper.hpp in Headers */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -247,8 +288,11 @@
 				CD3AC6FD1D2C06B5002B4BB0 /* shader.cpp in Sources */,
 				CD3AC7191D2C0950002B4BB0 /* shader_program.cpp in Sources */,
 				CD3AC6F21D2C03B7002B4BB0 /* material.cpp in Sources */,
+				CD62FD1E2292412900376440 /* renderer.cxx in Sources */,
 				CD3AC6F81D2C0518002B4BB0 /* texture.cpp in Sources */,
 				CD3AC7261D2C0C63002B4BB0 /* object.cpp in Sources */,
+				CD62FCF82290DC9000376440 /* opengl_helper.cxx in Sources */,
+				CD62FD232292C76B00376440 /* opengl_renderer.cxx in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -304,7 +348,7 @@
 				MTL_ENABLE_DEBUG_INFO = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = macosx;
-				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include/ $(PROJECT_DIR)/../include/expect/include/ $(PROJECT_DIR)/../math/ $(PROJECT_DIR)/../include/ $(PROJECT_DIR)/../math/include/ $(PROJECT_DIR)/../";
+				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include/ $(PROJECT_DIR)/../include/expect/include/ $(PROJECT_DIR)/../math/ $(PROJECT_DIR)/../include/ $(PROJECT_DIR)/../math/include/ $(PROJECT_DIR)/../util/include";
 			};
 			name = Debug;
 		};
@@ -350,7 +394,7 @@
 				MACOSX_DEPLOYMENT_TARGET = 10.10;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				SDKROOT = macosx;
-				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include/ $(PROJECT_DIR)/../include/expect/include/ $(PROJECT_DIR)/../math/ $(PROJECT_DIR)/../include/ $(PROJECT_DIR)/../math/include/ $(PROJECT_DIR)/../";
+				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include/ $(PROJECT_DIR)/../include/expect/include/ $(PROJECT_DIR)/../math/ $(PROJECT_DIR)/../include/ $(PROJECT_DIR)/../math/include/ $(PROJECT_DIR)/../util/include";
 			};
 			name = Release;
 		};

+ 20 - 3
graphics/include/game/graphics/material.hpp

@@ -7,16 +7,33 @@
 
 #pragma once
 
-#include "util/identity.hpp"
+#include <vector>
 
+#include "game/math/math_fwd.hpp"
+#include "game/util/identity.hpp"
 #include "shader_program.hpp"
 
 namespace graphics {
+  class texture;
+  struct uniform {
+    flyweight<texture> texture;
+    int uniform_id; // TODO (sjaffe): use an enum and hide remapping?
+  };
+
   class material : public identity<material> {
   public:
-    program const shaders;
+    shader_program const shaders;
+    std::vector<uniform> uniforms;
+
+  public:
+    static flyweight<material> create(shader_program const & sp,
+                                      std::string const & texture,
+                                      std::string const & uniform);
+
+    math::vec2i size() const;
+    void activate() const;
 
   private:
-    material(unsigned int, program);
+    material(unsigned int, shader_program const &);
   };
 }

+ 12 - 10
graphics/include/game/graphics/object.hpp

@@ -7,22 +7,24 @@
 
 #pragma once
 
+#include <vector>
+
 #include "game/math/math_fwd.hpp"
 #include "game/math/shape.hpp"
+#include "game/util/flyweight.hpp"
 #include "vector/vector.hpp"
 
-#include "material.hpp"
-
 namespace graphics {
-  struct bound {
-    math::vec2 corner;
-    math::vec2 size;
-  };
+  class material;
+  struct vertex;
 
   struct object {
-    bound location;
-    math::quad points;
-    material material;
-    bound frame;
+    math::dim2::rectangle location;
+    math::dim2::quad points;
+    flyweight<material> material;
+    math::dim2::rectangle frame;
   }; // size:56, align:4
+
+  void vertices(std::vector<vertex> & out, object const & obj);
+  void vertices(std::vector<vertex> & out, std::vector<object> const & obj);
 }

+ 61 - 0
graphics/include/game/graphics/renderer.hpp

@@ -0,0 +1,61 @@
+//
+//  renderer.hpp
+//  graphics
+//
+//  Created by Sam Jaffe on 5/19/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include "game/math/math_fwd.hpp"
+#include "game/util/flyweight.hpp"
+#include "game/util/hash.hpp"
+#include "vector/vector.hpp"
+
+namespace graphics {
+  class material;
+  struct renderer_impl;
+  struct vertex;
+
+  struct renderer {
+    virtual ~renderer() {}
+    virtual void draw(flyweight<material>, math::matr4 const &,
+                      std::vector<vertex> const &) = 0;
+    virtual void clear() = 0;
+    virtual void flush() = 0;
+  };
+
+  enum class driver { openGL };
+
+  class direct_renderer : public renderer {
+  public:
+    direct_renderer(driver d);
+    void draw(flyweight<material>, math::matr4 const &,
+              std::vector<vertex> const &) override;
+    void clear() override;
+    void flush() override;
+
+  private:
+    renderer_impl * pimpl;
+  };
+
+  class batch_renderer : public renderer {
+  public:
+    batch_renderer(renderer * impl, std::size_t batch_size = 0);
+    void draw(flyweight<material>, math::matr4 const &,
+              std::vector<vertex> const &) override;
+    void clear() override { impl_->clear(); }
+    void flush() override;
+
+  private:
+    void check();
+    void write();
+
+  private:
+    renderer * impl_;
+    std::unordered_map<flyweight<material>, std::vector<vertex>> batches_;
+    std::size_t batch_size_;
+    std::size_t elements_;
+  };
+}

+ 13 - 2
graphics/include/game/graphics/shader.hpp

@@ -7,12 +7,23 @@
 
 #pragma once
 
-#include "util/identity.hpp"
+#include <string>
+
+#include "game/util/flyweight.hpp"
 
 namespace graphics {
+  namespace shaders {
+    enum class type : unsigned int;
+  }
   class shader : public identity<shader> {
   public:
+    shaders::type type;
+    std::string path;
+
+  public:
+    static flyweight<shader> create(shaders::type tp, std::string const & path);
+
   private:
-    shader(unsigned int);
+    shader(unsigned int, shaders::type, std::string const &);
   };
 }

+ 15 - 3
graphics/include/game/graphics/shader_program.hpp

@@ -7,12 +7,24 @@
 
 #pragma once
 
-#include "util/identity.hpp"
+#include <string>
+
+#include "game/util/flyweight.hpp"
+
+#include "shader.hpp"
 
 namespace graphics {
-  class program : public identity<program> {
+  class shader_program : public identity<shader_program> {
   public:
+    flyweight<shader> fragment_shader;
+    flyweight<shader> vertex_shader;
+
+  public:
+    static flyweight<shader_program> create(std::string const & frag,
+                                            std::string const & vert);
+    void activate() const;
+
   private:
-    program(unsigned int);
+    shader_program(unsigned int, flyweight<shader>, flyweight<shader>);
   };
 }

+ 5 - 5
graphics/include/game/graphics/texture.hpp

@@ -8,22 +8,22 @@
 #pragma once
 
 #include "game/math/math_fwd.hpp"
-#include "util/identity.hpp"
+#include "game/util/flyweight.hpp"
 #include "vector/vector.hpp"
 
 namespace graphics {
   class texture : public identity<texture> {
   public:
-    static texture create(std::string const & imagefile);
-
     static texture const WHITE;
     static texture const DARK_YELLOW;
     static texture const LIGHT_BLUE;
-
     math::vec2i const size;
 
+  public:
+    static flyweight<texture> create(std::string const & imagefile);
+
   private:
-    static texture create(unsigned char *, math::vec2i);
+    static texture create(char const *, math::vec2i);
     texture(unsigned int, math::vec2i = math::vec2i{{0, 0}});
   };
 }

+ 19 - 0
graphics/include/game/graphics/vertex.h

@@ -0,0 +1,19 @@
+//
+//  vertex.h
+//  graphics
+//
+//  Created by Sam Jaffe on 5/20/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include "game/math/math_fwd.hpp"
+#include "vector/vector.hpp"
+
+namespace graphics {
+  struct vertex {
+    math::vec2 position, texture_coords;
+    math::rgba color;
+  };
+}

+ 46 - 0
graphics/src/helper.hpp

@@ -0,0 +1,46 @@
+//
+//  helper.hpp
+//  graphics
+//
+//  Created by Sam Jaffe on 5/18/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include <string>
+#include <utility>
+
+#include "game/math/math_fwd.hpp"
+
+template <typename> class flyweight;
+
+namespace graphics {
+  class shader;
+  class shader_program;
+  struct uniform;
+  namespace textures {
+    enum class format { RGB, RGBA };
+    unsigned int init(format, math::vec2i, void const *);
+  }
+  namespace shaders {
+    enum class type : unsigned int { FRAGMENT, VERTEX };
+    unsigned int init(type, std::string const &);
+    unsigned int init(flyweight<shader> const & fragmentShader,
+                      flyweight<shader> const & vertexShader);
+    void activate(unsigned int id);
+    int uniform_location(unsigned int id, std::string const & uniform);
+  }
+  namespace materials {
+    void activate(flyweight<shader_program> program,
+                  std::vector<uniform> const & uniforms);
+  }
+}
+
+namespace std {
+  template <> struct hash<graphics::shaders::type> {
+    std::size_t operator()(graphics::shaders::type tp) const {
+      return std::hash<unsigned int>()(static_cast<unsigned int>(tp));
+    }
+  };
+}

+ 56 - 0
graphics/src/material.cpp

@@ -6,3 +6,59 @@
 //
 
 #include "game/graphics/material.hpp"
+
+#include "game/graphics/texture.hpp"
+#include "game/util/hash.hpp"
+#include "helper.hpp"
+
+using namespace graphics;
+
+namespace {
+  using key_t = std::tuple<flyweight<shader_program>, std::string, std::string>;
+  std::unordered_map<key_t, flyweight<material>> g_materials;
+}
+
+static math::vec2i ZERO{{0, 0}};
+
+flyweight<texture> get_texture(std::string const & texture,
+                               std::string const & uniform) {
+  if (!texture.empty()) {
+    try {
+      return texture::create(texture);
+    } catch (std::exception const & e) {
+      // TODO: Logging
+    }
+  }
+  if (uniform == "u_normalMap") {
+    return texture::LIGHT_BLUE;
+  } else if (uniform == "u_specularMap") {
+    return texture::DARK_YELLOW;
+  } else if (uniform == "u_diffuseMap") {
+    return texture::WHITE;
+  }
+  throw;
+}
+
+flyweight<material> material::create(shader_program const & sp,
+                                     std::string const & texture,
+                                     std::string const & uniform) {
+  key_t key = std::make_tuple(sp, texture, uniform);
+  auto found = g_materials.find(key);
+  if (found != g_materials.end()) { return found->second; }
+
+  static unsigned int id{0};
+  material mat{++id, sp};
+  mat.uniforms.push_back({get_texture(texture, uniform),
+                          shaders::uniform_location(sp.id, uniform)});
+  flyweight<material> fly{id, mat};
+  return g_materials.emplace(key, fly).first->second;
+}
+
+material::material(unsigned int id, shader_program const & sp)
+    : identity<material>(id), shaders(sp) {}
+
+math::vec2i material::size() const {
+  return uniforms.empty() ? ZERO : uniforms.front().texture.actual().size;
+}
+
+void material::activate() const { materials::activate(shaders, uniforms); }

+ 29 - 0
graphics/src/object.cpp

@@ -6,3 +6,32 @@
 //
 
 #include "game/graphics/object.hpp"
+
+#include "game/graphics/vertex.h"
+
+using namespace graphics;
+
+void vertices(std::vector<vertex> & out, object const & obj) {
+  static const math::rgba CLEAR{{255, 255, 255, 255}};
+  math::dim2::quad frame_quad = obj.frame;
+
+  out.emplace_back(vertex{obj.points.ll, frame_quad.ll, CLEAR});
+  out.emplace_back(vertex{obj.points.lr, frame_quad.lr, CLEAR});
+  out.emplace_back(vertex{obj.points.ur, frame_quad.ur, CLEAR});
+  out.emplace_back(vertex{obj.points.ll, frame_quad.ll, CLEAR});
+  out.emplace_back(vertex{obj.points.ur, frame_quad.ur, CLEAR});
+  out.emplace_back(vertex{obj.points.ul, frame_quad.ul, CLEAR});
+}
+
+void graphics::vertices(std::vector<vertex> & out, object const & obj) {
+  out.reserve(out.size() + 6);
+  ::vertices(out, obj);
+}
+
+void graphics::vertices(std::vector<vertex> & out,
+                        std::vector<object> const & objs) {
+  out.reserve(out.size() + 6 * objs.size());
+  for (object const & obj : objs) {
+    ::vertices(out, obj);
+  }
+}

+ 356 - 0
graphics/src/opengl_helper.cxx

@@ -0,0 +1,356 @@
+//
+//  helper.cxx
+//  graphics
+//
+//  Created by Sam Jaffe on 5/18/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include "helper.hpp"
+
+#include <iostream>
+#include <memory>
+#include <sstream>
+#ifdef __APPLE__
+#include <OpenGL/gl3.h>
+#endif
+
+#include "scope_guard/scope_guard.hpp"
+#include "vector/vector.hpp"
+
+#include "game/graphics/material.hpp"
+#include "game/graphics/shader.hpp"
+#include "game/util/env.hpp"
+#include "game/util/files.hpp"
+
+namespace {
+  typedef void (*GLGetIntFor)(GLuint, GLenum, GLint *);
+  typedef void (*GLGetLog)(GLuint, GLsizei, GLsizei *, GLchar *);
+  struct error_formatter {
+    void operator()() const;
+    static void shader_error(GLuint id, std::string const & path);
+    static void shader_program_error(GLuint id, std::string const & frag_path,
+                                     std::string const & vert_path);
+
+    GLuint obj;
+    std::string fileName, path, eNoticeMessage, eFileMessage, eWindowTitle;
+    GLGetIntFor getIntv;
+    GLGetLog getInfoLog;
+  };
+
+  void error_formatter::operator()() const {
+    std::stringstream errorString;
+    std::string errorLineNumber;
+
+    int infologLength = 0;
+    int maxLength = 0;
+
+    getIntv(obj, GL_INFO_LOG_LENGTH, &maxLength);
+    errorString << eNoticeMessage << "\n";
+
+    std::unique_ptr<char[]> infoLog(new char[maxLength + 1]);
+    getInfoLog(obj, maxLength, &infologLength, infoLog.get());
+
+    std::string errorLog(infoLog.get());
+    infoLog.reset();
+    if (infologLength <= 0) return;
+
+    std::size_t openParen = errorLog.find('(');
+    std::size_t closeParen = errorLog.find(')');
+    errorLineNumber =
+        errorLog.substr(openParen + 1, closeParen - openParen - 1);
+
+    errorString << fileName << " , "
+                << "line " << errorLineNumber << ":\n";
+
+    std::string errorStart = errorLog.find("error") != std::string::npos
+                                 ? errorLog.substr(errorLog.find("error"))
+                                 : errorLog;
+
+    errorString << errorLog << "\n";
+
+    errorString << "OpenGL Version: " << glGetString(GL_VERSION) << "\n";
+    errorString << "GLSL Shading Version: "
+                << glGetString(GL_SHADING_LANGUAGE_VERSION) << "\n";
+
+    errorString << eFileMessage << "\n";
+    errorString << "RAW ERROR LOG: " << errorLog;
+
+    std::stringstream consoleOutput;
+
+    consoleOutput << "1>" << path << '(' << errorLineNumber << ')' << ": "
+                  << errorStart
+                  << "in OpenGL version: " << glGetString(GL_VERSION) << " and "
+                  << "GLSL Shading Version: "
+                  << glGetString(GL_SHADING_LANGUAGE_VERSION) << std::endl;
+
+#ifdef _WIN32
+    OutputDebugStringA(consoleOutput.str().c_str());
+    MessageBoxA(nullptr, errorString.str().c_str(), eWindowTitle.c_str(),
+                MB_OK);
+#else
+    std::cerr << consoleOutput.str() << std::endl;
+    std::cerr << errorString.str() << std::endl;
+#endif
+  }
+
+  void error_formatter::shader_error(GLuint id, std::string const & path) {
+    std::string fileName = path.substr(path.find_last_of("/"));
+    error_formatter{id,
+                    fileName,
+                    path,
+                    "GLSL shader compile error!",
+                    "File location" + path,
+                    "GLSL Compile Error in " + fileName,
+                    glGetShaderiv,
+                    glGetShaderInfoLog}();
+  }
+
+  void error_formatter::shader_program_error(GLuint id,
+                                             std::string const & frag_path,
+                                             std::string const & vert_path) {
+    std::string fileName = frag_path.substr(frag_path.find_last_of("/"));
+    error_formatter{id,
+                    fileName,
+                    frag_path,
+                    "GLSL program link error!",
+                    "File location of vertex shader: " + vert_path +
+                        "\nFile location of fragment shader: " + frag_path,
+                    "GLSL Link Error",
+                    glGetProgramiv,
+                    glGetProgramInfoLog}();
+  }
+}
+
+namespace graphics { namespace textures {
+  static int glfmt(format color_fmt) {
+    switch (color_fmt) {
+    case format::RGB:
+      return GL_RGB;
+    case format::RGBA:
+      return GL_RGBA;
+    }
+  }
+
+  unsigned int init(format color_fmt, math::vec2i dimension,
+                    void const * data) {
+    unsigned int id;
+    // Enable texturings
+    //    glEnable( GL_TEXTURE_2D );
+
+    // Tell OpenGL that our pixel data is single-byte aligned
+    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+
+    // Ask OpenGL for an unused texName (ID number) to use for this texture
+    glGenTextures(1, &id);
+
+    // Tell OpenGL to bind (set) this as the currently active texture
+    glBindTexture(GL_TEXTURE_2D, id);
+    // Set texture clamp vs. wrap (repeat)
+
+    // one of: GL_CLAMP_TO_EDGE, GL_REPEAT, GL_MIRRORED_REPEAT,
+    // GL_MIRROR_CLAMP_TO_EDGE, ...
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+    // the format our source pixel data is currently in; any of: GL_RGB,
+    // GL_RGBA, GL_LUMINANCE, GL_LUMINANCE_ALPHA, ...
+    int bufferFormat = glfmt(color_fmt);
+
+    // the format we want the texture to me on the card; allows us to translate
+    // into a different texture format as we upload to OpenGL
+    int internalFormat = bufferFormat;
+
+    /* glTexImage2D: Load a 2d texture image
+     * target: Creating this as a 2d texture.
+     * level: Which mipmap level to use as the "root" (0 = the highest-quality,
+     *  full-res image), if mipmaps are enabled.
+     * internalFormat: Type of texel format we want OpenGL to use for this
+     *  texture internally on the video card.
+     * width: Texel-width of image; for maximum compatibility, use 2^N + 2^B,
+     *  where N is some integer in the range [3,10], and B is the border
+     *  thickness [0,1]
+     * height: Texel-height of image; for maximum compatibility, use 2^M + 2^B,
+     *  where M is some integer in the range [3,10], and B is the border
+     *  thickness [0,1]
+     * border: Border size, in texels (must be 0 or 1)
+     * format: Pixel format describing the composition of the pixel data in
+     *  buffer
+     * type: Pixel color components are unsigned bytes (one byte per color/alpha
+     *  channel)
+     * pixels: Location of the actual pixel data bytes/buffer
+     */
+    glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, dimension.x(), dimension.y(),
+                 0, bufferFormat, GL_UNSIGNED_BYTE, data);
+
+    glGenerateMipmap(GL_TEXTURE_2D);
+
+    // Set magnification (texel > pixel) and minification (texel < pixel)
+    // filters one of: GL_NEAREST, GL_LINEAR
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    // one of: GL_NEAREST, GL_LINEAR, GL_NEAREST_MIPMAP_NEAREST,
+    // GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST,
+    // GL_LINEAR_MIPMAP_LINEAR
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
+                    GL_NEAREST_MIPMAP_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 5);
+
+    return id;
+  }
+}}
+
+namespace graphics { namespace shaders {
+  struct file_read_error : std::runtime_error {
+    using std::runtime_error::runtime_error;
+  };
+
+  struct compilation_error : std::runtime_error {
+    using std::runtime_error::runtime_error;
+  };
+
+  struct linker_error : std::runtime_error {
+    using std::runtime_error::runtime_error;
+  };
+
+  static int gltype(type tp) {
+    switch (tp) {
+    case type::FRAGMENT:
+      return GL_FRAGMENT_SHADER;
+    case type::VERTEX:
+      return GL_VERTEX_SHADER;
+    }
+  }
+
+  unsigned int init(type tp, std::string const & path) {
+    std::unique_ptr<char const[]> buffer;
+
+    // 1. Load the vertex shader code (text file) to a new memory buffer
+    std::string const abs_path = env::resource_file(path);
+    if ((buffer = files::load(abs_path))) {
+      throw file_read_error("Could not load shader file " + abs_path);
+    }
+
+    // 2. Create a new shader ID
+    // GL_VERTEX_SHADER or GL_FRAGMENT_SHADER
+    unsigned int id = glCreateShader(gltype(tp));
+
+    // 3. Associate the shader code with the new shader ID
+    char const * buffer_ptr = buffer.get();
+    glShaderSource(id, 1, &buffer_ptr, nullptr);
+
+    // 4. Compile the shader (the shader compiler is built in to your graphics
+    //    card driver)
+    glCompileShader(id);
+
+    // 5. Check for compile errors
+    int return_code;
+    glGetShaderiv(id, GL_COMPILE_STATUS, &return_code);
+
+    if (return_code != GL_TRUE) {
+      error_formatter::shader_error(id, abs_path);
+      throw compilation_error("Could not compile the shader file");
+    }
+    return id;
+  }
+
+  unsigned int init(flyweight<shader> const & fragmentShader,
+                    flyweight<shader> const & vertexShader) {
+    // 9. Create a new shader program ID
+    unsigned int id = glCreateProgram();
+
+    // 10. Attach the vertex and fragment shaders to the new shader program
+    glAttachShader(id, vertexShader.id);
+    glAttachShader(id, fragmentShader.id);
+
+    // 11. Do some other advanced stuff we'll do later on (like setting generic
+    // vertex attrib locations)
+    glBindAttribLocation(id, 0, "a_position");
+    glBindAttribLocation(id, 1, "a_color");
+    glBindAttribLocation(id, 2, "a_texCoords");
+    glBindAttribLocation(id, 3, "a_normal");
+
+    // 12. Link the program
+    glLinkProgram(id);
+
+    // 13. Check for link errors
+    int return_code;
+    glGetProgramiv(id, GL_LINK_STATUS, &return_code);
+
+    if (return_code != GL_TRUE) {
+      error_formatter::shader_program_error(id, fragmentShader.actual().path,
+                                            vertexShader.actual().path);
+      throw linker_error("Could not link shader program");
+    }
+    return id;
+  }
+
+  void activate(unsigned int id) {
+    // 100. Use the shader program ID to "turn it on" for all subsequent drawing
+    glUseProgram(id);
+
+    /*
+     // 101. Enable texturing and Bind texture(s) to GPU texture units
+     glActiveTexture(GL_TEXTURE3);
+     glEnable(GL_TEXTURE_2D);
+     glBindTexture(GL_TEXTURE_2D, emissiveTextureID);
+
+     glActiveTexture(GL_TEXTURE2);
+     glEnable(GL_TEXTURE_2D);
+     glBindTexture(GL_TEXTURE_2D, specularTextureID);
+
+     glActiveTexture(GL_TEXTURE1);
+     glEnable(GL_TEXTURE_2D);
+     glBindTexture(GL_TEXTURE_2D, normalTextureID);
+
+     glActiveTexture(GL_TEXTURE0);
+     glEnable(GL_TEXTURE_2D);
+     glBindTexture(GL_TEXTURE_2D, diffuseTextureID);
+     */
+
+    // 102. Get the location # of each named uniform you wish to pass in to the
+    // shader
+    int timeUniformLocation = glGetUniformLocation(id, "u_time");
+    int scale = glGetUniformLocation(id, "Scale");
+    int diffuseMapUniformLocation = glGetUniformLocation(id, "u_diffuseMap");
+    int normalMapUniformLocation = glGetUniformLocation(id, "u_normalMap");
+    int specularMapUniformLocation = glGetUniformLocation(id, "u_specularMap");
+    int emissiveMapUniformLocation = glGetUniformLocation(id, "u_emissiveMap");
+    int debugWave = glGetUniformLocation(id, "g_debugWave");
+
+    // 103. Set the uniform values, including the texture unit numbers for
+    // texture (sampler) uniforms
+    // Env::GetCurrentTimeSeconds()
+    glUniform1f(timeUniformLocation, (float)time(NULL));
+    glUniform1f(scale, 2.f);
+    glUniform1i(debugWave, 1); // TODO: m_waveEffectOn in ShaderProgram??
+    // for GL_TEXTURE0, texture unit 0
+    glUniform1i(diffuseMapUniformLocation, 0);
+    // for GL_TEXTURE1, texture unit 1
+    glUniform1i(normalMapUniformLocation, 1);
+    // for GL_TEXTURE2, texture unit 2
+    glUniform1i(specularMapUniformLocation, 2);
+    // for GL_TEXTURE3, texture unit 3
+    glUniform1i(emissiveMapUniformLocation, 3);
+  }
+
+  int uniform_location(unsigned int id, std::string const & uniform) {
+    return glGetUniformLocation(id, uniform.c_str());
+  }
+}}
+
+namespace graphics { namespace materials {
+  void activate(flyweight<shader_program> program,
+                std::vector<uniform> const & uniforms) {
+    glUseProgram(program.id);
+
+    for (unsigned int i = 0; i < uniforms.size(); i++) {
+      const uniform & uniform = uniforms[i];
+      glActiveTexture(i + GL_TEXTURE0);
+      //      glEnable(GL_TEXTURE_2D);
+      glBindTexture(GL_TEXTURE_2D, uniform.texture.id);
+      glUniform1i(uniform.uniform_id, i);
+    }
+
+    glActiveTexture(GL_TEXTURE0);
+  }
+}}

+ 115 - 0
graphics/src/opengl_renderer.cxx

@@ -0,0 +1,115 @@
+//
+//  opengl_renderer.cxx
+//  graphics
+//
+//  Created by Sam Jaffe on 5/20/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include "renderer_impl.hpp"
+
+#ifdef __APPLE__
+#include <OpenGL/gl3.h>
+#endif
+
+#include "game/graphics/material.hpp"
+#include "game/graphics/vertex.h"
+#include "matrix/matrix.hpp"
+#include "matrix/matrix_helpers.hpp"
+
+using namespace graphics;
+
+class opengl_renderer : public renderer_impl {
+public:
+  opengl_renderer();
+  ~opengl_renderer();
+
+  void draw(flyweight<material>, math::matr4 const &,
+            std::vector<vertex> const &) override;
+  void clear() override;
+  void flush() override;
+
+private:
+  const math::matr4 identity{math::matrix::identity<float, 4>()};
+  math::matr4 world_to_clip{identity};
+  double current_time{0.0};
+  unsigned int vertex_array_object{0}, vertex_buffer_object{0};
+};
+
+opengl_renderer::opengl_renderer() {
+  glGenVertexArrays(1, &vertex_array_object);
+  glBindVertexArray(vertex_array_object);
+  glGenBuffers(1, &vertex_buffer_object);
+  glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object);
+}
+
+opengl_renderer::~opengl_renderer() {
+  glDeleteBuffers(1, &vertex_buffer_object);
+  glDeleteVertexArrays(1, &vertex_array_object);
+}
+
+void opengl_renderer::clear() {
+  world_to_clip = identity;
+
+  // vec2i resolution = env::resolution();
+  // orthogonal_view(0.0, resolution[0], 0.0, resolution[1], -1.0, 1.0);
+
+  glClearDepth(1.f);
+  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+  glClearColor(0.f, 0.f, 0.0f, 1.f);
+  glEnable(GL_DEPTH_TEST);
+
+  // TODO: Use a unified time calculation
+  current_time = time(NULL);
+
+  // TODO: ???
+  // scale(0.5, 0.5, 1); // Don't know why I need to do this
+}
+
+void opengl_renderer::flush() { glFlush(); }
+
+void opengl_renderer::draw(flyweight<material> material,
+                           math::matr4 const & object_to_world,
+                           std::vector<vertex> const & vertices) {
+  // TODO: Don't activate here?
+  auto const & mat = material.actual();
+  // TODO: Attatch shader-id to material-id
+  unsigned int const id = mat.shaders.id;
+  mat.activate();
+
+  int objectLocation = glGetUniformLocation(id, "u_objectToWorldMatrix");
+  glUniformMatrix4fv(objectLocation, 1, false, &object_to_world(0, 0));
+
+  int clipLocation = glGetUniformLocation(id, "u_worldToClipMatrix");
+  glUniformMatrix4fv(clipLocation, 1, false, &world_to_clip(0, 0));
+
+  int timeLocation = glGetUniformLocation(id, "u_time");
+  glUniform1d(timeLocation, current_time);
+
+  // TODO: Cache attribute locations
+  int positionLocation = glGetAttribLocation(id, "a_position");
+  int colorLocation = glGetAttribLocation(id, "a_color");
+  int texCoordsLocation = glGetAttribLocation(id, "a_texCoords");
+
+  glEnableVertexAttribArray(positionLocation);
+  glEnableVertexAttribArray(colorLocation);
+  glEnableVertexAttribArray(texCoordsLocation);
+
+  glVertexAttribPointer(positionLocation, 2, GL_FLOAT, GL_FALSE, sizeof(vertex),
+                        &vertices[0].position);
+  glVertexAttribPointer(colorLocation, 4, GL_UNSIGNED_BYTE, GL_TRUE,
+                        sizeof(vertex), &vertices[0].color);
+  glVertexAttribPointer(texCoordsLocation, 2, GL_FLOAT, GL_FALSE,
+                        sizeof(vertex), &vertices[0].texture_coords);
+
+  glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(vertices.size()));
+
+  glDisableVertexAttribArray(positionLocation);
+  glDisableVertexAttribArray(colorLocation);
+  glDisableVertexAttribArray(texCoordsLocation);
+}
+
+template <> renderer_impl * graphics::get_renderer_impl<driver::openGL>() {
+  static opengl_renderer impl;
+  return &impl;
+}

+ 63 - 0
graphics/src/renderer.cxx

@@ -0,0 +1,63 @@
+//
+//  renderer.cxx
+//  graphics
+//
+//  Created by Sam Jaffe on 5/19/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include "game/graphics/renderer.hpp"
+#include <vector>
+
+#include "game/graphics/vertex.h"
+#include "matrix/matrix.hpp"
+#include "renderer_impl.hpp"
+
+using namespace graphics;
+
+renderer_impl * get_renderer_impl(driver d) {
+  switch (d) {
+  case driver::openGL:
+    return get_renderer_impl<driver::openGL>();
+  default:
+    throw;
+  }
+}
+
+direct_renderer::direct_renderer(driver d) : pimpl(::get_renderer_impl(d)) {}
+
+void direct_renderer::draw(flyweight<material> material, math::matr4 const &,
+                           std::vector<vertex> const & verts) {
+  pimpl->draw(material, {}, verts);
+}
+
+void direct_renderer::clear() { pimpl->clear(); }
+void direct_renderer::flush() { pimpl->flush(); }
+
+batch_renderer::batch_renderer(renderer * impl, std::size_t batch_size)
+    : impl_(impl), batches_(), batch_size_(batch_size), elements_(0) {}
+
+// TODO (sjaffe): object-to-world matrix...
+void batch_renderer::draw(flyweight<material> material, math::matr4 const &,
+                          std::vector<vertex> const & verts) {
+  auto & batch_verts = batches_[material];
+  batch_verts.insert(batch_verts.end(), verts.begin(), verts.end());
+  check();
+}
+
+void batch_renderer::flush() {
+  write();
+  impl_->flush();
+}
+
+void batch_renderer::check() {
+  if (batch_size_ && elements_ >= batch_size_) { write(); }
+}
+
+void batch_renderer::write() {
+  for (auto & pair : batches_) {
+    impl_->draw(pair.first, math::matr4(), pair.second);
+  }
+  batches_.clear();
+  elements_ = 0;
+}

+ 28 - 0
graphics/src/renderer_impl.hpp

@@ -0,0 +1,28 @@
+//
+//  renderer_impl.hpp
+//  graphics
+//
+//  Created by Sam Jaffe on 5/20/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include "game/graphics/renderer.hpp"
+#include "game/math/math_fwd.hpp"
+#include "game/util/flyweight.hpp"
+
+namespace graphics {
+  template <driver> renderer_impl * get_renderer_impl();
+
+  class material;
+  struct vertex;
+
+  struct renderer_impl {
+    virtual ~renderer_impl() {}
+    virtual void draw(flyweight<material>, math::matr4 const &,
+                      std::vector<vertex> const &) = 0;
+    virtual void clear() = 0;
+    virtual void flush() = 0;
+  };
+}

+ 25 - 0
graphics/src/shader.cpp

@@ -6,3 +6,28 @@
 //
 
 #include "game/graphics/shader.hpp"
+
+#include <unordered_map>
+
+#include "game/util/hash.hpp"
+#include "helper.hpp"
+
+using namespace graphics;
+
+namespace {
+  using key_t = std::pair<shaders::type, std::string>;
+  std::unordered_map<key_t, flyweight<shader>> g_shaders;
+}
+
+flyweight<shader> shader::create(shaders::type tp, std::string const & path) {
+  auto key = std::make_pair(tp, path);
+  auto found = g_shaders.find(key);
+  if (found != g_shaders.end()) { return found->second; }
+
+  auto id = shaders::init(tp, path);
+  flyweight<shader> fly{id, {id, tp, path}};
+  return g_shaders.emplace(key, fly).first->second;
+}
+
+shader::shader(unsigned int id, shaders::type type, std::string const & path)
+    : identity<shader>(id), type(type), path(path) {}

+ 35 - 0
graphics/src/shader_program.cpp

@@ -6,3 +6,38 @@
 //
 
 #include "game/graphics/shader_program.hpp"
+
+#include <unordered_map>
+#include <utility>
+
+#include "game/graphics/shader.hpp"
+#include "game/util/hash.hpp"
+#include "helper.hpp"
+
+using namespace graphics;
+
+namespace {
+  using key_t = std::pair<std::string, std::string>;
+  std::unordered_map<key_t, flyweight<shader_program>> g_shader_programs;
+}
+
+flyweight<shader_program> shader_program::create(std::string const & frag,
+                                                 std::string const & vert) {
+  auto key = std::make_pair(frag, vert);
+  auto found = g_shader_programs.find(key);
+  if (found != g_shader_programs.end()) { return found->second; }
+
+  auto fragment_shader = shader::create(shaders::type::FRAGMENT, frag);
+  auto vertex_shader = shader::create(shaders::type::VERTEX, vert);
+
+  auto id = shaders::init(fragment_shader, vertex_shader);
+  flyweight<shader_program> fly{id, {id, fragment_shader, vertex_shader}};
+  return g_shader_programs.emplace(key, fly).first->second;
+}
+
+shader_program::shader_program(unsigned int id, flyweight<shader> frag,
+                               flyweight<shader> vert)
+    : identity<shader_program>(id), fragment_shader(frag), vertex_shader(vert) {
+}
+
+void shader_program::activate() const { shaders::activate(id); }

+ 41 - 44
graphics/src/texture.cpp

@@ -7,65 +7,62 @@
 
 #include "game/graphics/texture.hpp"
 
-#include "scope_guard/scope_guard.hpp"
-
-#include <string>
 #include <unordered_map>
 
+#include "scope_guard/scope_guard.hpp"
+
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wcomma"
 #define STB_IMAGE_IMPLEMENTATION
 #include "stb/stb_image.h"
 #pragma clang diagnostic pop
 
+#include "game/util/hash.hpp"
+#include "helper.hpp"
+
 unsigned char * stbi_load(char const *, int *, int *, int *, int);
 void stbi_image_free(void *);
 
-template <typename T, typename Key = std::string> class private_factory {
-public:
-  using key_t = Key;
-
-  template <typename... Args> static T create(key_t const & key) {
-    auto found = instances.find(key);
-    if (found == instances.end()) {
-      found = instances.emplace(key, T::create(key)).first;
-    }
-    return found->second;
-  }
-
-private:
-  static std::unordered_map<key_t, T> instances;
-};
-
-namespace graphics { namespace detail { namespace texture {
-  struct format {};
+using namespace graphics;
 
-  std::unordered_map<std::string, ::graphics::texture> g_textures;
+namespace {
+  std::unordered_map<std::string, flyweight<texture>> g_textures;
+}
 
-  unsigned int init(format = {}, math::vec2i = {}, unsigned char * = {}) {
-    throw; // TODO implement
+static textures::format format(int comps) {
+  switch (comps) {
+  case 3:
+    return textures::format::RGB;
+  case 4:
+    return textures::format::RGBA;
+  default:
+    throw;
   }
-}}}
-
-namespace graphics {
-  texture texture::create(std::string const & imagefile) {
-    using detail::texture::g_textures;
-    auto found = g_textures.find(imagefile);
-    if (found != g_textures.end()) { return found->second; }
+}
 
-    int components = 0;
-    math::vec2i size;
-    unsigned char * data =
-        stbi_load(imagefile.c_str(), &size.x(), &size.y(), &components, 0);
-    scope(exit) { stbi_image_free(data); };
-    texture tex{detail::texture::init({}, size, data), size};
-    return g_textures.emplace(imagefile, std::move(tex)).first->second;
-  }
+flyweight<texture> texture::create(std::string const & imagefile) {
+  auto found = g_textures.find(imagefile);
+  if (found != g_textures.end()) { return found->second; }
 
-  texture texture::create(unsigned char * data, math::vec2i size) {
-    return {detail::texture::init({}, size, data), size};
-  }
+  int components = 0;
+  math::vec2i size;
+  unsigned char * data =
+      stbi_load(imagefile.c_str(), &size.x(), &size.y(), &components, 0);
+  scope(exit) { stbi_image_free(data); };
+  auto id = textures::init(format(components), size, data);
+  flyweight<texture> fly{id, {id, size}};
+  return g_textures.emplace(imagefile, fly).first->second;
+}
 
-  texture::texture(unsigned int id, math::vec2i sz)
-      : identity<graphics::texture>(id), size(sz) {}
+texture texture::create(char const * data, math::vec2i size) {
+  return {textures::init(format(4), size, data), size};
 }
+
+texture::texture(unsigned int id, math::vec2i sz)
+    : identity<texture>(id), size(sz) {}
+
+texture const texture::WHITE = texture::create("\xFF\xFF\xFF\xFF", {{1, 1}});
+texture const texture::DARK_YELLOW =
+    texture::create("\x80\x80\x00\xFF", {{1, 1}});
+texture const texture::LIGHT_BLUE =
+    texture::create("\x80\x80\xFF\xFF", {{1, 1}});

+ 36 - 5
util/gameutils.xcodeproj/project.pbxproj

@@ -7,7 +7,8 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
-		CD3C808C1D6646AF00ACC795 /* identity.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CD3AC7161D2C0794002B4BB0 /* identity.hpp */; };
+		CD62FD04229195FF00376440 /* files.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD62FD02229195FF00376440 /* files.cxx */; };
+		CD62FD082291988F00376440 /* osx_env.mm in Sources */ = {isa = PBXBuildFile; fileRef = CD62FD072291988F00376440 /* osx_env.mm */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -43,8 +44,11 @@
 
 /* Begin PBXFileReference section */
 		CD3AC7081D2C0726002B4BB0 /* libgameutils.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libgameutils.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
-		CD3AC7161D2C0794002B4BB0 /* identity.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = identity.hpp; sourceTree = "<group>"; };
 		CD62FCE622904AD500376440 /* GoogleMock.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GoogleMock.xcodeproj; path = "../../gmock-xcode-master/GoogleMock.xcodeproj"; sourceTree = "<group>"; };
+		CD62FCFC2291951400376440 /* linux_env.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = linux_env.cxx; sourceTree = "<group>"; };
+		CD62FCFF2291953700376440 /* game */ = {isa = PBXFileReference; lastKnownFileType = folder; name = game; path = include/game; sourceTree = "<group>"; };
+		CD62FD02229195FF00376440 /* files.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = files.cxx; sourceTree = "<group>"; };
+		CD62FD072291988F00376440 /* osx_env.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = osx_env.mm; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -62,6 +66,7 @@
 			isa = PBXGroup;
 			children = (
 				CD62FCE622904AD500376440 /* GoogleMock.xcodeproj */,
+				CD62FCFF2291953700376440 /* game */,
 				CD3AC70A1D2C0726002B4BB0 /* src */,
 				CD3AC7091D2C0726002B4BB0 /* Products */,
 			);
@@ -78,9 +83,11 @@
 		CD3AC70A1D2C0726002B4BB0 /* src */ = {
 			isa = PBXGroup;
 			children = (
-				CD3AC7161D2C0794002B4BB0 /* identity.hpp */,
+				CD62FCFC2291951400376440 /* linux_env.cxx */,
+				CD62FD072291988F00376440 /* osx_env.mm */,
+				CD62FD02229195FF00376440 /* files.cxx */,
 			);
-			name = src;
+			path = src;
 			sourceTree = "<group>";
 		};
 		CD62FCE722904AD500376440 /* Products */ = {
@@ -101,7 +108,6 @@
 			isa = PBXHeadersBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				CD3C808C1D6646AF00ACC795 /* identity.hpp in Headers */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -115,6 +121,7 @@
 				CD3AC7041D2C0726002B4BB0 /* Sources */,
 				CD3AC7051D2C0726002B4BB0 /* Frameworks */,
 				CD3AC7061D2C0726002B4BB0 /* Headers */,
+				CD62FD002291955400376440 /* ShellScript */,
 			);
 			buildRules = (
 			);
@@ -193,11 +200,33 @@
 		};
 /* End PBXReferenceProxy section */
 
+/* Begin PBXShellScriptBuildPhase section */
+		CD62FD002291955400376440 /* ShellScript */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+			);
+			outputFileListPaths = (
+			);
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "mkdir -p ${BUILT_PRODUCTS_DIR}/usr/local/include/\ncp -r ${PROJECT_DIR}/include/* ${BUILT_PRODUCTS_DIR}/usr/local/include/\n";
+		};
+/* End PBXShellScriptBuildPhase section */
+
 /* Begin PBXSourcesBuildPhase section */
 		CD3AC7041D2C0726002B4BB0 /* Sources */ = {
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CD62FD082291988F00376440 /* osx_env.mm in Sources */,
+				CD62FD04229195FF00376440 /* files.cxx in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -254,6 +283,7 @@
 				MTL_ENABLE_DEBUG_INFO = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = macosx;
+				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include $(PROJECT_DIR)/../include/";
 			};
 			name = Debug;
 		};
@@ -300,6 +330,7 @@
 				MACOSX_DEPLOYMENT_TARGET = 10.10;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				SDKROOT = macosx;
+				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include $(PROJECT_DIR)/../include/";
 			};
 			name = Release;
 		};

+ 15 - 0
util/include/game/util/env.hpp

@@ -0,0 +1,15 @@
+//
+//  env.hpp
+//  gameutils
+//
+//  Created by Sam Jaffe on 5/19/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include <string>
+
+namespace env {
+  std::string resource_file(std::string const & relative_path);
+}

+ 16 - 0
util/include/game/util/files.hpp

@@ -0,0 +1,16 @@
+//
+//  files.hpp
+//  gameutils
+//
+//  Created by Sam Jaffe on 5/19/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include <memory>
+#include <string>
+
+namespace files {
+  std::unique_ptr<char[]> load(std::string const & absolute_path);
+}

+ 33 - 0
util/include/game/util/flyweight.hpp

@@ -0,0 +1,33 @@
+//
+//  flyweight.hpp
+//  gameutils
+//
+//  Created by Sam Jaffe on 5/19/19.
+//
+
+#pragma once
+
+#include <unordered_map>
+
+#include "identity.hpp"
+
+template <typename T> class flyweight {
+private:
+  static std::unordered_map<unsigned int, T> actual_;
+
+public:
+  const unsigned int id;
+
+public:
+  flyweight(unsigned int id, T actual) : id(id) {
+    actual_.emplace(id, std::move(actual));
+  }
+  flyweight(identity<T> actual) : id(actual.id) {}
+  T const & actual() const { return actual_.find(this->id)->second; }
+
+  friend bool operator==(flyweight lhs, flyweight rhs) {
+    return lhs.id == rhs.id;
+  }
+};
+
+template <typename T> std::unordered_map<unsigned int, T> flyweight<T>::actual_;

+ 52 - 0
util/include/game/util/hash.hpp

@@ -0,0 +1,52 @@
+//
+//  hash.hpp
+//  gameutils
+//
+//  Created by Sam Jaffe on 5/19/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include <tuple>
+#include <utility>
+
+template <typename> class flyweight;
+
+namespace std {
+  template <typename T> struct hash<flyweight<T>> {
+    std::size_t operator()(flyweight<T> tp) const {
+      return std::hash<unsigned int>()(tp.id);
+    }
+  };
+
+  template <typename T, typename S> struct hash<std::pair<T, S>> {
+    std::size_t operator()(std::pair<T, S> const & pair) const {
+      return std::hash<T>()(pair.first) ^ std::hash<S>()(pair.second);
+    }
+  };
+
+  template <std::size_t I> struct tuple_hash;
+
+  template <> struct tuple_hash<0> {
+    template <typename... As>
+    std::size_t operator()(std::tuple<As...> const &) const {
+      return 0;
+    }
+  };
+
+  template <std::size_t I> struct tuple_hash {
+    template <typename... As>
+    std::size_t operator()(std::tuple<As...> const & tuple) const {
+      using elt = typename std::tuple_element<I - 1, std::tuple<As...>>::type;
+      return std::hash<elt>()(std::get<I - 1>(tuple)) ^
+             tuple_hash<I - 1>()(tuple);
+    }
+  };
+
+  template <typename... As> struct hash<std::tuple<As...>> {
+    std::size_t operator()(std::tuple<As...> const & tuple) const {
+      return tuple_hash<sizeof...(As)>()(tuple);
+    }
+  };
+}

util/identity.hpp → util/include/game/util/identity.hpp


+ 36 - 0
util/src/files.cxx

@@ -0,0 +1,36 @@
+//
+//  files.cxx
+//  gameutils
+//
+//  Created by Sam Jaffe on 5/19/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include "game/util/files.hpp"
+
+#include "scope_guard/scope_guard.hpp"
+
+namespace files {
+  std::unique_ptr<char[]> load(std::string const & absolute_path) {
+    FILE * fp = fopen(absolute_path.c_str(), "r");
+    if (!fp) { return nullptr; }
+
+    scope(exit) { fclose(fp); };
+    // Determine file size
+    fseek(fp, 0, SEEK_END);
+    long size = ftell(fp);
+    if (size < 0) { return nullptr; }
+
+    std::unique_ptr<char[]> buffer{new char[size + 1]};
+
+    rewind(fp);
+    size_t read = fread(buffer.get(), sizeof(char), size, fp);
+    if (read != static_cast<size_t>(size)) {
+      fputs("Error reading file", stderr);
+      return nullptr;
+    } else {
+      buffer[read + 1] = '\0';
+    }
+    return buffer;
+  }
+}

+ 9 - 0
util/src/linux_env.cxx

@@ -0,0 +1,9 @@
+//
+//  linux_env.cxx
+//  gameutils
+//
+//  Created by Sam Jaffe on 5/19/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include "game/util/env.hpp"

+ 35 - 0
util/src/osx_env.mm

@@ -0,0 +1,35 @@
+//
+//  osx_env.mm
+//  gameutils
+//
+//  Created by Sam Jaffe on 5/19/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include "game/util/env.hpp"
+
+#import <Cocoa/Cocoa.h>
+
+namespace {
+  NSUInteger encoding = NSUTF8StringEncoding;
+  NSString* translate(std::string const & str) {
+    return [NSString stringWithCString:str.c_str() encoding:encoding];
+  }
+}
+
+namespace env {
+  std::string resource_file(std::string const& path) {
+    size_t dir_idx = path.find_last_of("/");
+    size_t ext_idx = path.find_first_of(".");
+    std::string base = path.substr(0, dir_idx);
+    std::string name = path.substr(dir_idx + 1, ext_idx - (dir_idx + 1));
+    std::string type = path.substr(ext_idx + 1);
+    
+    NSString* url = [[NSBundle mainBundle] pathForResource:translate(name)
+                                                    ofType:translate(type)
+                                               inDirectory:translate(base)];
+    
+    char const* abs_path = [url cStringUsingEncoding:encoding];
+    return abs_path ? std::string(abs_path) : std::string();
+  }
+}