diff --git a/jars/joml-1.8.2.jar b/jars/joml-1.8.2.jar
new file mode 100644
index 00000000..cdacef21
Binary files /dev/null and b/jars/joml-1.8.2.jar differ
diff --git a/pom.xml b/pom.xml
index 9ec1006b..ba1d9ad1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -203,6 +203,11 @@
json
20201115
+
+ org.joml
+ joml
+ 1.8.2
+
diff --git a/src/main/java/org/qme/client/vis/gl/Mesh.java b/src/main/java/org/qme/client/vis/gl/Mesh.java
new file mode 100644
index 00000000..81540f6d
--- /dev/null
+++ b/src/main/java/org/qme/client/vis/gl/Mesh.java
@@ -0,0 +1,128 @@
+package org.qme.client.vis.gl;
+
+import org.lwjgl.system.MemoryUtil;
+import org.qme.client.vis.tex.TextureManager;
+import org.qme.io.Logger;
+import org.qme.io.Severity;
+
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+
+import static org.lwjgl.opengl.GL33.*;
+
+/**
+ * A mesh with texture coordinates, position coordinates, etc. This contains the
+ * data OpenGL actually uses to render stuff.
+ */
+public final class Mesh {
+
+ /**
+ * A bunch of IDs! vaoID is the ID of all of the data's "linking point", so
+ * to speak. All of the xxxVboID are the IDs are various sets of data GL
+ * uses.
+ */
+ private final int vaoId,
+ posVboId, idxVboId, texVboId,
+ vertexCount
+ ;
+
+ /**
+ * Set up all of the arrays into buffers and such.
+ * @param positions the locations that things get drawn at
+ * @param indices the indices of the position buffer to draw, so we don't
+ * repeat data.
+ * @param texPositions the texture coordinates to use.
+ */
+ public Mesh(float[] positions, int[] indices, float[] texPositions) {
+
+ // The buffers we'll place the data in
+ FloatBuffer verticesBuffer, texBuffer;
+ IntBuffer indexBuffer;
+
+ vertexCount = indices.length;
+
+ vaoId = glGenVertexArrays();
+ glBindVertexArray(vaoId);
+
+ // Vertices
+ posVboId = glGenBuffers();
+ verticesBuffer = MemoryUtil.memAllocFloat(positions.length);
+ verticesBuffer.put(positions).flip();
+ glBindBuffer(GL_ARRAY_BUFFER, posVboId);
+ glBufferData(GL_ARRAY_BUFFER, verticesBuffer, GL_STATIC_DRAW);
+ glVertexAttribPointer(0, 2, GL_FLOAT, false, 0, 0);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ MemoryUtil.memFree(verticesBuffer);
+
+ // Indices
+ idxVboId = glGenBuffers();
+ indexBuffer = MemoryUtil.memAllocInt(indices.length);
+ indexBuffer.put(indices).flip();
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, idxVboId);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexBuffer, GL_STATIC_DRAW);
+ MemoryUtil.memFree(indexBuffer);
+
+ // Texture coordinates
+ texVboId = glGenBuffers();
+ texBuffer = MemoryUtil.memAllocFloat(texPositions.length);
+ texBuffer.put(texPositions).flip();
+ glBindBuffer(GL_ARRAY_BUFFER, texVboId);
+ glBufferData(GL_ARRAY_BUFFER, texBuffer, GL_STATIC_DRAW);
+ glVertexAttribPointer(1, 2, GL_FLOAT, false, 0, 0);
+
+ // We're not placing data into the vao anymore
+ glBindVertexArray(0);
+
+ }
+
+ /**
+ * Draw this mesh using the texture given.
+ * TODO: optimize this so we don't load and unload the same texture 3721897482394 times.
+ * @param textureName which texture
+ */
+ public void render(String textureName) {
+
+ // Activate first texture unit
+ glActiveTexture(GL_TEXTURE0);
+ // Bind the texture (this line is expensive to run)
+ Integer texID = TextureManager.getTexture(textureName);
+ if (texID == null) {
+ Logger.log("Attempted to load nonexistent texture! Texture: " + textureName, Severity.FATAL);
+ }
+ glBindTexture(GL_TEXTURE_2D, texID);
+
+ // Draw the mesh
+ glBindVertexArray(vaoId);
+ glEnableVertexAttribArray(0);
+ glEnableVertexAttribArray(1);
+ glDrawElements(GL_TRIANGLES, vertexCount, GL_UNSIGNED_INT, 0);
+
+ // Clear everything for next mesh
+ glDisableVertexAttribArray(0);
+ glBindVertexArray(0);
+
+ }
+
+ /**
+ * Clear all of the mesh data. Because OpenGL deals with this, Java does not
+ * garbage collect so please call this manually.
+ */
+ public void delete() {
+
+ glDisableVertexAttribArray(0);
+
+ // Clear the VBO
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+
+ // Flush all data
+ glDeleteBuffers(posVboId);
+ glDeleteBuffers(idxVboId);
+ glDeleteBuffers(texVboId);
+
+ // Clear the VAO
+ glBindVertexArray(0);
+ glDeleteVertexArrays(vaoId);
+
+ }
+
+}
diff --git a/src/main/java/org/qme/client/vis/gl/Shader.java b/src/main/java/org/qme/client/vis/gl/Shader.java
new file mode 100644
index 00000000..472967cf
--- /dev/null
+++ b/src/main/java/org/qme/client/vis/gl/Shader.java
@@ -0,0 +1,188 @@
+package org.qme.client.vis.gl;
+
+import org.joml.Matrix4f;
+import org.lwjgl.system.MemoryStack;
+
+import java.nio.FloatBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.lwjgl.opengl.GL33.*;
+
+/**
+ * Yeah, Cameron, I know you're going to be reading this frantically trying to
+ * figure out how shaders work. Or someone else, maybe. So I'm writing it here
+ * the way I understand.
+ *
+ * OpenGL can compile GLSL code into GPU-assembly language. You can then bind
+ * the shader so sets of vertices are rendered with it. OpenGL renders shaders
+ * by compiling a String of source code, which we generate on-the-fly from a
+ * file. The vertex shader (_vert.glsl) is first, which produces the position of
+ * the vertex being processed. I *think* the fragment shader makes the output
+ * color? Or something? Idk. The "real" rendering work is done by meshes.
+ *
+ * @author adamhutchings
+ * @since 0.4
+ */
+public class Shader {
+
+ private final int programId, vertId, fragId;
+
+ private final Map uniforms = new HashMap<>();
+
+ /**
+ * Load a string from a file.
+ * @param fileName the path to the file
+ */
+ public static String loadFileAsString(String fileName) throws Exception {
+ return Files.readString(Path.of(fileName));
+ }
+
+ /**
+ * Load a shader from files.
+ * @param fileBase load vertex code from src/shader/fileBase_vert.glsl
+ * and src/shader/fileBase_frag.glsl
+ */
+ public Shader(String fileBase) throws Exception {
+
+ programId = glCreateProgram();
+ if (programId == 0) {
+ throw new Exception("Unable to create parent shader");
+ }
+
+ vertId = createShader(
+ loadFileAsString("src/shader/" + fileBase + "_vert.glsl"), GL_VERTEX_SHADER
+ );
+ fragId = createShader(
+ loadFileAsString("src/shader/" + fileBase + "_frag.glsl"), GL_FRAGMENT_SHADER
+ );
+
+ link();
+
+ }
+
+ /**
+ * Link everything together.
+ */
+ private void link() throws Exception {
+
+ glLinkProgram(programId);
+ if (glGetProgrami(programId, GL_LINK_STATUS) == 0) {
+ throw new Exception("Error linking shader code: " + glGetProgramInfoLog(programId, 1024));
+ }
+
+ // These pieces of shaders aren't "needed" anymore, so we can detach
+ // them from the final linked program.
+ if (vertId != 0) {
+ glDetachShader(programId, vertId);
+ }
+ if (fragId != 0) {
+ glDetachShader(programId, fragId);
+ }
+
+ glValidateProgram(programId);
+ if (glGetProgrami(programId, GL_VALIDATE_STATUS) == 0) {
+ // THESE WARNINGS ARE NOT NECESSARILY A SIGN OF A FAILING COMPILATION.
+ // Just so y'all know :D
+ System.err.println("Warning validating Shader code: " + glGetProgramInfoLog(programId, 1024));
+ }
+
+ }
+
+ /**
+ * Create an individual shader of a given type.
+ */
+ private int createShader(String code, int type) throws Exception {
+
+ int shaderId = glCreateShader(type);
+
+ if (shaderId == 0) {
+ throw new Exception("Could not create shader of type " + type);
+ }
+
+ glShaderSource(shaderId, code);
+ glCompileShader(shaderId);
+
+ if (glGetShaderi(shaderId, GL_COMPILE_STATUS) == 0) {
+ throw new Exception("Error compiling shader code: " + glGetShaderInfoLog(shaderId, 1024));
+ }
+
+ glAttachShader(programId, shaderId);
+
+ return shaderId;
+
+ }
+
+ /**
+ * Set this as the shader to be used.
+ */
+ public void bind() {
+ glUseProgram(programId);
+ }
+
+ /**
+ * Stop using this shader.
+ */
+ public void unbind() {
+ glUseProgram(0);
+ }
+
+ /**
+ * Call this to delete the shader.
+ */
+ public void cleanup() {
+ unbind();
+ // This check should technically be unnecessary.
+ if (programId != 0) {
+ glDeleteProgram(programId);
+ }
+ }
+
+ /**
+ * For us to pass data into the shader from the program, we need to create a
+ * "uniform", which like all other OpenGL code is set using integers. Here
+ * we interface with it using strings so that we can access it more intuitively
+ * and easily.
+ * @param uniformName the name of the new uniform to create
+ * @throws Exception if the uniform is not defined *in the shader program*.
+ */
+ public void createUniform(String uniformName) throws Exception {
+ int uniformLocation = glGetUniformLocation(programId,
+ uniformName);
+ if (uniformLocation < 0) {
+ throw new Exception("Could not find uniform:" +
+ uniformName);
+ }
+ uniforms.put(uniformName, uniformLocation);
+ }
+
+ /**
+ * Set a uniform to an int value
+ * @param uniformName which uniform
+ * @param value the value to set it to
+ */
+ public void setUniform(String uniformName, int value) {
+ glUniform1i(uniforms.get(uniformName), value);
+ }
+
+ /**
+ * Set a uniform to an float value
+ * @param uniformName which uniform
+ * @param value the value to set it to
+ */
+ public void setUniform(String uniformName, float value) {
+ glUniform1f(uniforms.get(uniformName), value);
+ }
+
+ /**
+ * Set a uniform to an float array value
+ * @param uniformName which uniform
+ * @param value the value to set it to
+ */
+ public void setUniform(String uniformName, float[] value) {
+ glUniform2fv(uniforms.get(uniformName), value);
+ }
+
+}