diff --git a/build.gradle b/build.gradle
index f9d55c3f..94778f9d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -27,6 +27,8 @@ ext {
slf4j_version = "1.7.25"
gson_version = "2.8.5"
jcip_annotation_version = "1.0"
+ joml_version = "1.10.0"
+ lwjgl_version = '3.2.3'
// Testing
junit_version = "4.12"
diff --git a/gestalt-graphics-core/build.gradle b/gestalt-graphics-core/build.gradle
new file mode 100644
index 00000000..247ef447
--- /dev/null
+++ b/gestalt-graphics-core/build.gradle
@@ -0,0 +1,24 @@
+// Copyright 2021 The Terasology Foundation
+// SPDX-License-Identifier: Apache-2.0
+apply from: "$rootDir/gradle/common.gradle"
+
+// Primary dependencies definition
+dependencies {
+ implementation "org.joml:joml:$joml_version"
+ implementation "com.google.guava:guava:$guava_version"
+ implementation 'com.googlecode.gentyref:gentyref:1.2.0'
+ implementation "org.slf4j:slf4j-api:$slf4j_version"
+ implementation "com.android.support:support-annotations:$android_annotation_version"
+ implementation "org.lwjgl:lwjgl:$lwjgl_version"
+ implementation "org.lwjgl:lwjgl-opengl:$lwjgl_version"
+
+ // These dependencies are only needed for running tests
+ testImplementation "junit:junit:$junit_version"
+ testImplementation "ch.qos.logback:logback-classic:$logback_version"
+ testImplementation "org.mockito:mockito-core:$mockito_version"
+
+ testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.2")
+ testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.2")
+ testImplementation("org.junit.jupiter:junit-jupiter-params:5.6.2")
+ testImplementation("junit:junit:4.12")
+}
diff --git a/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/Color.java b/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/Color.java
new file mode 100644
index 00000000..a6874fc1
--- /dev/null
+++ b/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/Color.java
@@ -0,0 +1,354 @@
+// Copyright 2021 The Terasology Foundation
+// SPDX-License-Identifier: Apache-2.0
+package org.terasology.gestalt.graphics;
+
+import org.joml.Math;
+import org.joml.Vector3fc;
+import org.joml.Vector3ic;
+import org.joml.Vector4fc;
+import org.joml.Vector4ic;
+
+import java.nio.ByteBuffer;
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * Color is a representation of a RGBA color. Color components can be set and accessed via floats ranging from 0-1, or ints ranging from 0-255.
+ * Color is immutable and thread safe.
+ *
+ * There are a plethora of Color classes, but none that are quite suitable IMO:
+ *
+ * - vecmaths - doesn't access with r/g/b/a, separation by representation is awkward, feature bland.
+ * - Slick2D - ideally will lose dependency on slick utils. Also ties to lwjgl
+ * - Lwjgl - don't want to be graphics implementation dependant
+ * - javafx - ew
+ * - com.sun.prism - double ew. Shouldn't use com.sun classes at all
+ * - awt - tempting, certainly feature-rich. Has some strange awt-specific functionality though (createContext) and native links
+ *
+ *
+ */
+public class Color implements Colorc {
+ public static final Colorc black = new Color(0x000000FF);
+ public static final Colorc white = new Color(0xFFFFFFFF);
+ public static final Colorc blue = new Color(0x0000FFFF);
+ public static final Colorc green = new Color(0x00FF00FF);
+ public static final Colorc red = new Color(0xFF0000FF);
+ public static final Colorc grey = new Color(0x888888FF);
+ public static final Colorc transparent = new Color(0x00000000);
+ public static final Colorc yellow = new Color(0xFFFF00FF);
+ public static final Colorc cyan = new Color(0x00FFFFFF);
+ public static final Colorc magenta = new Color(0xFF00FFFF);
+
+
+ private static final int MAX = 255;
+ private static final int RED_OFFSET = 24;
+ private static final int GREEN_OFFSET = 16;
+ private static final int BLUE_OFFSET = 8;
+ private static final int RED_FILTER = 0x00FFFFFF;
+ private static final int GREEN_FILTER = 0xFF00FFFF;
+ private static final int BLUE_FILTER = 0xFFFF00FF;
+ private static final int ALPHA_FILTER = 0xFFFFFF00;
+
+ private int representation;
+
+ /**
+ * Creates a color that is black with full alpha.
+ */
+ public Color() {
+ representation = 0x000000FF;
+ }
+
+ /**
+ * range between 0x00000000 to 0xFFFFFFFF
+ *
+ * @param representation color in hex format
+ */
+ public Color(int representation) {
+ this.representation = representation;
+ }
+
+ /**
+ * set the color source
+ *
+ * @param src color source
+ */
+ public Color(Colorc src) {
+ this.set(src.rgba());
+ }
+
+ /**
+ * Create a color with the given red/green/blue values. Alpha is initialised as max.
+ *
+ * @param r red in the range of 0.0f to 1.0f
+ * @param g green in the range of 0.0f to 1.0f
+ * @param b blue in the range of 0.0f to 1.0f
+ */
+ public Color(float r, float g, float b) {
+ this((int) (r * MAX), (int) (g * MAX), (int) (b * MAX));
+ }
+
+ /**
+ * Creates a color with the given red/green/blue/alpha values.
+ *
+ * @param r red in the range of 0.0f to 1.0f
+ * @param g green in the range of 0.0f to 1.0f
+ * @param b blue in the range of 0.0f to 1.0f
+ * @param a alpha in the range of 0.0f to 1.0f
+ */
+ public Color(float r, float g, float b, float a) {
+ this((int) (r * MAX), (int) (g * MAX), (int) (b * MAX), (int) (a * MAX));
+ }
+
+ /**
+ * Creates a color with the given red/green/blue values. Alpha is initialised as max.
+ *
+ * @param r red in the range of 0.0f to 1.0f
+ * @param g green in the range of 0.0f to 1.0f
+ * @param b blue in the range of 0.0f to 1.0f
+ */
+ public Color(int r, int g, int b) {
+ this.set(r, g, b);
+ }
+
+ /**
+ * Creates a color with the given red/green/blue/alpha values.
+ *
+ * @param r red in the range of 0 to 255
+ * @param g green in the range of 0 to 255
+ * @param b blue in the range of 0 to 255
+ * @param a alpha in the range of 0 to 255
+ */
+ public Color(int r, int g, int b, int a) {
+ this.set(r, g, b, a);
+ }
+
+ @Override
+ public int r() {
+ return (representation >> RED_OFFSET) & MAX;
+ }
+
+ @Override
+ public int g() {
+ return (representation >> GREEN_OFFSET) & MAX;
+ }
+
+ @Override
+ public int b() {
+ return (representation >> BLUE_OFFSET) & MAX;
+ }
+
+ @Override
+ public int a() {
+ return representation & MAX;
+ }
+
+ @Override
+ public float rf() {
+ return r() / 255.f;
+ }
+
+ @Override
+ public float bf() {
+ return b() / 255.f;
+ }
+
+ @Override
+ public float gf() {
+ return g() / 255.f;
+ }
+
+ @Override
+ public float af() {
+ return a() / 255.f;
+ }
+
+
+ public Color set(Vector3ic representation) {
+ return this.set(representation.x(),
+ representation.y(),
+ representation.z());
+ }
+
+ public Color set(Vector3fc representation) {
+ return this.set((int) (representation.x() * MAX),
+ (int) (representation.y() * MAX),
+ (int) (representation.z() * MAX));
+ }
+
+
+ public Color set(Vector4fc representation) {
+ return this.set((int) (representation.x() * MAX),
+ (int) (representation.y() * MAX),
+ (int) (representation.z() * MAX),
+ (int) (representation.w() * MAX));
+ }
+
+ public Color set(Vector4ic representation) {
+ return this.set(representation.x(),
+ representation.y(),
+ representation.z(),
+ representation.w());
+ }
+
+ public Color set(int representation) {
+ this.representation = representation;
+ return this;
+ }
+
+ public Color set(int r, int g, int b, int a) {
+ return this.set(Math.clamp(0, 255, r) << RED_OFFSET |
+ Math.clamp(0, 255, g) << GREEN_OFFSET |
+ Math.clamp(0, 255, b) << BLUE_OFFSET |
+ Math.clamp(0, 255, a));
+ }
+
+
+ public Color set(int r, int g, int b) {
+ return this.set(r, g, b, 0xFF);
+ }
+
+
+ /**
+ * set the value of the red channel
+ *
+ * @param value color range between 0-255
+ * @return this
+ */
+ public Color setRed(int value) {
+ return this.set(Math.clamp(0, 255, value) << RED_OFFSET | (representation & RED_FILTER));
+ }
+
+ /**
+ * set the value of the red channel
+ *
+ * @param value color range between 0.0f to 1.0f
+ * @return this
+ */
+ public Color setRed(float value) {
+ return setRed((int) (value * MAX));
+ }
+
+ /**
+ * set the value of the green channel
+ *
+ * @param value color range between 0-255
+ * @return this
+ */
+ public Color setGreen(int value) {
+ return this.set(Math.clamp(0, 255, value) << GREEN_OFFSET | (representation & GREEN_FILTER));
+ }
+
+
+ /**
+ * set the value of the green channel
+ *
+ * @param value color range between 0.0f to 1.0f
+ * @return this
+ */
+ public Color setGreen(float value) {
+ return setGreen((int) (value * MAX));
+ }
+
+
+ /**
+ * set the value of the blue channel
+ *
+ * @param value blue range between 0-255
+ * @return this
+ */
+ public Color setBlue(int value) {
+ return this.set(Math.clamp(0, 255, value) << BLUE_OFFSET | (representation & BLUE_FILTER));
+ }
+
+ /**
+ * set the value of the blue channel
+ *
+ * @param value blue range between 0.0f to 1.0f
+ * @return this
+ */
+ public Color setBlue(float value) {
+ return setBlue((int) (value * MAX));
+ }
+
+ /**
+ * set the value of the alpha channel
+ *
+ * @param value alpha range between 0-255
+ * @return this
+ */
+ public Color setAlpha(int value) {
+ return this.set(Math.clamp(0, 255, value) | (representation & ALPHA_FILTER));
+ }
+
+ /**
+ * set the value of the alpha channel
+ *
+ * @param value alpha range between 0.0f to 1.0f
+ * @return this
+ */
+ public Color setAlpha(float value) {
+ return setAlpha((int) (value * MAX));
+ }
+
+
+ /**
+ * 255 Subtract from all components except alpha;
+ *
+ * @return this
+ */
+ public Color invert() {
+ return this.set((~representation & ALPHA_FILTER) | a());
+ }
+
+ @Override
+ public int rgba() {
+ return representation;
+ }
+
+ @Override
+ public int rgb() {
+ return (representation & ALPHA_FILTER) | 0xFF;
+ }
+
+ /**
+ * write color to ByteBuffer as int.
+ *
+ * @param buffer The ByteBuffer
+ */
+ public void addToBuffer(ByteBuffer buffer) {
+ buffer.putInt(representation);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj instanceof Color) {
+ Color other = (Color) obj;
+ return representation == other.representation;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(representation);
+ }
+
+ @Override
+ public String toHex() {
+ StringBuilder builder = new StringBuilder();
+ String hexString = Integer.toHexString(representation);
+ for (int i = 0; i < 8 - hexString.length(); ++i) {
+ builder.append('0');
+ }
+ builder.append(hexString.toUpperCase(Locale.ENGLISH));
+ return builder.toString();
+ }
+
+ @Override
+ public String toString() {
+ return toHex();
+ }
+}
diff --git a/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/Colorc.java b/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/Colorc.java
new file mode 100644
index 00000000..a11f85ab
--- /dev/null
+++ b/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/Colorc.java
@@ -0,0 +1,64 @@
+package org.terasology.gestalt.graphics;
+
+/**
+ * Interface to a read-only view of a Color.
+ */
+public interface Colorc {
+ /**
+ * The internal representation of the color
+ * @return hex representation
+ */
+ int rgba();
+
+ /**
+ * the internal representation of the color with alpha channel set to 0xFF
+ * @return hex representation
+ */
+ int rgb();
+
+ /**
+ * @return The red component, between 0 and 255
+ */
+ int r();
+
+ /**
+ * @return The green component, between 0 and 255
+ */
+ int g();
+
+ /**
+ * @return The blue component, between 0 and 255
+ */
+ int b();
+
+ /**
+ * @return The alpha component, between 0 and 255
+ */
+ int a();
+
+ /**
+ * @return The red channel, between 0.0f and 1.0f
+ */
+ float rf();
+
+ /**
+ * @return The green channel, between 0.0f and 1.0f
+ */
+ float gf();
+
+ /**
+ * @return The blue channel, between 0.0f and 1.0f
+ */
+ float bf();
+
+ /**
+ * @return The alpha channel, between 0.0f and 1.0f
+ */
+ float af();
+
+ /**
+ * the hex representation of color as a String
+ * @return the hex representation
+ */
+ String toHex();
+}
diff --git a/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/rendering/opengl/GLAttributes.java b/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/rendering/opengl/GLAttributes.java
new file mode 100644
index 00000000..9d71fd49
--- /dev/null
+++ b/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/rendering/opengl/GLAttributes.java
@@ -0,0 +1,168 @@
+// Copyright 2021 The Terasology Foundation
+// SPDX-License-Identifier: Apache-2.0
+
+package org.terasology.gestalt.graphics.rendering.opengl;
+
+import org.joml.Vector2f;
+import org.joml.Vector2fc;
+import org.joml.Vector3f;
+import org.joml.Vector3fc;
+import org.joml.Vector4f;
+import org.joml.Vector4fc;
+import org.terasology.gestalt.graphics.Color;
+import org.terasology.gestalt.graphics.Colorc;
+import org.terasology.gestalt.graphics.resource.VertexAttribute;
+import org.terasology.gestalt.graphics.resource.VertexResource;
+
+import java.nio.ByteBuffer;
+
+public final class GLAttributes {
+ private GLAttributes() {
+
+ }
+
+ public static final VertexAttribute VECTOR_3_F_VERTEX_ATTRIBUTE = new VertexAttribute(Vector3f.class, new VertexAttribute.AttributeConfiguration() {
+
+ @Override
+ public void write(Vector3fc value, int vertIdx, int offset, VertexResource resource) {
+ int bufferStart = vertIdx * resource.getInStride() + offset;
+ ByteBuffer buffer = resource.buffer();
+ buffer.putFloat(bufferStart, value.x());
+ buffer.putFloat(bufferStart + Float.BYTES, value.y());
+ buffer.putFloat(bufferStart + Float.BYTES * 2, value.z());
+ }
+
+ @Override
+ public Vector3f read(int vertIdx, int offset, VertexResource resource, Vector3f dest) {
+ int bufferStart = vertIdx * resource.getInStride() + offset;
+ ByteBuffer buffer = resource.buffer();
+ dest.x = buffer.getFloat(bufferStart);
+ dest.y = buffer.getFloat(bufferStart + Float.BYTES);
+ dest.z = buffer.getFloat(bufferStart + Float.BYTES * 2);
+ return dest;
+ }
+
+ @Override
+ public int size(int vertIdx, int offset, VertexResource resource) {
+ return (vertIdx * resource.getInStride() + offset) + Float.BYTES * 3;
+ }
+
+ @Override
+ public int numElements(int offset, VertexResource resource) {
+ int size = (resource.getInSize() / resource.getInStride());
+ if (resource.getInSize() % resource.getInStride() >= Float.BYTES * 3) {
+ size++;
+ }
+ return size;
+ }
+ }, VertexAttribute.TypeMapping.ATTR_FLOAT, 3);
+
+ public static final VertexAttribute VECTOR_4_F_VERTEX_ATTRIBUTE = new VertexAttribute<>(Vector4f.class, new VertexAttribute.AttributeConfiguration() {
+ @Override
+ public void write(Vector4fc value, int vertIdx, int offset, VertexResource resource) {
+ int bufferStart = vertIdx * resource.getInStride() + offset;
+ ByteBuffer buffer = resource.buffer();
+ buffer.putFloat(bufferStart, value.x());
+ buffer.putFloat(bufferStart + Float.BYTES, value.y());
+ buffer.putFloat(bufferStart + Float.BYTES * 2, value.z());
+ buffer.putFloat(bufferStart + Float.BYTES * 3, value.w());
+ }
+
+ @Override
+ public Vector4f read(int vertIdx, int offset, VertexResource resource, Vector4f dest) {
+ int bufferStart = vertIdx * resource.getInStride() + offset;
+ ByteBuffer buffer = resource.buffer();
+ dest.x = buffer.getFloat(bufferStart);
+ dest.y = buffer.getFloat(bufferStart + Float.BYTES);
+ dest.z = buffer.getFloat(bufferStart + Float.BYTES * 2);
+ dest.w = buffer.getFloat(bufferStart + Float.BYTES * 3);
+ return dest;
+ }
+
+ @Override
+ public int size(int vertIdx, int offset, VertexResource resource) {
+ return (vertIdx * resource.getInStride() + offset) + Float.BYTES * 4;
+ }
+
+ @Override
+ public int numElements(int offset, VertexResource resource) {
+ int size = (resource.getInSize() / resource.getInStride());
+ if (resource.getInSize() % resource.getInStride() >= Float.BYTES * 4) {
+ size++;
+ }
+ return size;
+ }
+ }, VertexAttribute.TypeMapping.ATTR_FLOAT, 4);
+
+ public static final VertexAttribute COLOR_4_F_VERTEX_ATTRIBUTE = new VertexAttribute(Color.class, new VertexAttribute.AttributeConfiguration() {
+ @Override
+ public void write(Colorc value, int vertIdx, int offset, VertexResource resource) {
+ int bufferStart = vertIdx * resource.getInStride() + offset;
+ ByteBuffer buffer = resource.buffer();
+
+ buffer.putFloat(bufferStart, value.rf());
+ buffer.putFloat(bufferStart + Float.BYTES, value.gf());
+ buffer.putFloat(bufferStart + Float.BYTES * 2, value.bf());
+ buffer.putFloat(bufferStart + Float.BYTES * 3, value.af());
+ }
+
+ @Override
+ public Color read(int vertIdx, int offset, VertexResource resource, Color dest) {
+ int bufferStart = vertIdx * resource.getInStride() + offset;
+ ByteBuffer buffer = resource.buffer();
+ dest.setRed(buffer.getFloat(bufferStart));
+ dest.setGreen(buffer.getFloat(bufferStart + Float.BYTES));
+ dest.setBlue(buffer.getFloat(bufferStart + Float.BYTES * 2));
+ dest.setAlpha(buffer.getFloat(bufferStart + Float.BYTES * 3));
+ return dest;
+ }
+
+ @Override
+ public int size(int vertIdx, int offset, VertexResource resource) {
+ return (vertIdx * resource.getInStride() + offset) + Float.BYTES * 4;
+ }
+
+ @Override
+ public int numElements(int offset, VertexResource resource) {
+ int size = (resource.getInSize() / resource.getInStride());
+ if (resource.getInSize() % resource.getInStride() >= (offset + Float.BYTES * 4)) {
+ size++;
+ }
+ return size;
+ }
+
+ }, VertexAttribute.TypeMapping.ATTR_FLOAT, 4);
+
+ public static final VertexAttribute VECTOR_2_F_VERTEX_ATTRIBUTE = new VertexAttribute<>(Vector2f.class, new VertexAttribute.AttributeConfiguration() {
+ @Override
+ public void write(Vector2fc value, int vertIdx, int offset, VertexResource resource) {
+ int bufferStart = vertIdx * resource.getInStride() + offset;
+ ByteBuffer buffer = resource.buffer();
+ buffer.putFloat(bufferStart, value.x());
+ buffer.putFloat(bufferStart + Float.BYTES, value.y());
+ }
+
+ @Override
+ public Vector2f read(int vertIdx, int offset, VertexResource resource, Vector2f dest) {
+ int bufferStart = vertIdx * resource.getInStride() + offset;
+ ByteBuffer buffer = resource.buffer();
+ dest.x = buffer.getFloat(bufferStart);
+ dest.y = buffer.getFloat(bufferStart + Float.BYTES);
+ return dest;
+ }
+
+ @Override
+ public int size(int vertIdx, int offset, VertexResource resource) {
+ return (vertIdx * resource.getInStride() + offset) + Float.BYTES * 2;
+ }
+
+ @Override
+ public int numElements(int offset, VertexResource resource) {
+ int size = (resource.getInSize() / resource.getInStride());
+ if (resource.getInSize() % resource.getInStride() >= Float.BYTES * 2) {
+ size++;
+ }
+ return size;
+ }
+ }, VertexAttribute.TypeMapping.ATTR_FLOAT, 2);
+}
diff --git a/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/resource/IndexResource.java b/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/resource/IndexResource.java
new file mode 100644
index 00000000..719920ee
--- /dev/null
+++ b/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/resource/IndexResource.java
@@ -0,0 +1,98 @@
+// Copyright 2021 The Terasology Foundation
+// SPDX-License-Identifier: Apache-2.0
+
+package org.terasology.gestalt.graphics.resource;
+
+import org.lwjgl.BufferUtils;
+
+import java.nio.ByteBuffer;
+
+/**
+ * defines the order of vertices to walk for rendering geometry
+ *
+ * refrence: https://www.khronos.org/opengl/wiki/Primitive
+ */
+public class IndexResource {
+ public ByteBuffer buffer;
+ private int numIndices = 0;
+ private int inSize = 0;
+ private int posIndex = 0;
+
+ public int getNumberOfIndices() {
+ return numIndices;
+ }
+
+ public int getSize() {
+ return numIndices * Integer.BYTES;
+ }
+
+ public IndexResource() {
+ this.buffer = BufferUtils.createByteBuffer(0);
+ }
+
+ public void ensureCapacity(int size) {
+ if (size > buffer.capacity()) {
+ int newCap = Math.max(this.inSize << 1, size);
+ ByteBuffer newBuffer = BufferUtils.createByteBuffer(newCap);
+ buffer.limit(Math.min(size, inSize));
+ buffer.position(0);
+ newBuffer.put(buffer);
+ this.buffer = newBuffer;
+ }
+ if (size > this.inSize) {
+ this.inSize = size;
+ }
+ }
+
+ public void copy(IndexResource resource) {
+ ensureCapacity(resource.inSize);
+ ByteBuffer copyBuffer = resource.buffer;
+ copyBuffer.limit(resource.getSize());
+ copyBuffer.rewind();
+ buffer.put(copyBuffer);
+
+ this.inSize = resource.inSize;
+ this.numIndices = resource.getNumberOfIndices();
+ }
+
+
+ public void reserveElements(int elements) {
+ ensureCapacity(elements * Integer.BYTES);
+ }
+
+ public void rewind() {
+ posIndex = 0;
+ }
+
+ public void put(int value) {
+ ensureCapacity((posIndex + 1) * Integer.BYTES);
+ buffer.putInt(posIndex * Integer.BYTES, value);
+ posIndex++;
+ if (posIndex > numIndices) {
+ numIndices = posIndex;
+ }
+ }
+
+ public void squeeze() {
+ if (this.inSize != buffer.capacity()) {
+ ByteBuffer newBuffer = BufferUtils.createByteBuffer(this.inSize);
+ buffer.limit(this.inSize);
+ buffer.position(0);
+ newBuffer.put(buffer);
+ this.buffer = newBuffer;
+ }
+ }
+
+ public void reallocateElements(int indices) {
+ ensureCapacity((indices + 1) * Integer.BYTES);
+ numIndices = indices;
+ }
+
+ public void position(int position) {
+ posIndex = position;
+ }
+
+ public void put(int index, int value) {
+ buffer.putInt(index * Integer.BYTES, value);
+ }
+}
diff --git a/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/resource/VertexAttribute.java b/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/resource/VertexAttribute.java
new file mode 100644
index 00000000..84b0741c
--- /dev/null
+++ b/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/resource/VertexAttribute.java
@@ -0,0 +1,59 @@
+// Copyright 2021 The Terasology Foundation
+// SPDX-License-Identifier: Apache-2.0
+
+package org.terasology.gestalt.graphics.resource;
+
+
+import org.lwjgl.opengl.GL30;
+
+/**
+ * attribute maps a target object or a primitive data to a {@link VertexResource}
+ *
+ * @param the target object
+ */
+public class VertexAttribute {
+
+ public final TypeMapping mapping;
+ public final int count;
+ public final Class type;
+ public final AttributeConfiguration configuration;
+
+ public interface AttributeConfiguration {
+ void write(T value, int vertIdx, int offset, VertexResource resource);
+
+ TImpl read(int vertIdx, int offset, VertexResource resource, TImpl dest);
+
+ int size(int vertIdx, int offset, VertexResource resource);
+
+ int numElements(int offset, VertexResource resource);
+ }
+
+ /**
+ * @param type the mapping type
+ * @param mapping maps a primitive to a given supported type.
+ * @param count the number elements that is described by the target
+ */
+ public VertexAttribute(Class type, AttributeConfiguration attributeConfiguration, TypeMapping mapping, int count) {
+ this.type = type;
+ this.mapping = mapping;
+ this.count = count;
+ this.configuration = attributeConfiguration;
+
+ }
+
+
+ public enum TypeMapping {
+ ATTR_FLOAT(Float.BYTES, GL30.GL_FLOAT),
+ ATTR_SHORT(Short.BYTES, GL30.GL_SHORT),
+ ATTR_BYTE(Byte.BYTES, GL30.GL_BYTE),
+ ATTR_INT(Integer.BYTES, GL30.GL_INT);
+
+ public final int size;
+ public final int glType;
+
+ TypeMapping(int size, int glType) {
+ this.size = size;
+ this.glType = glType;
+ }
+ }
+}
diff --git a/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/resource/VertexAttributeBinding.java b/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/resource/VertexAttributeBinding.java
new file mode 100644
index 00000000..0cf483d5
--- /dev/null
+++ b/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/resource/VertexAttributeBinding.java
@@ -0,0 +1,81 @@
+// Copyright 2021 The Terasology Foundation
+// SPDX-License-Identifier: Apache-2.0
+
+package org.terasology.gestalt.graphics.resource;
+
+/**
+ * a binding that maps depending on the type of attribute and a resource where the data is committed to
+ * @param
+ */
+public class VertexAttributeBinding {
+ private final VertexResource resource;
+ private final VertexAttribute attribute;
+ private final int offset;
+ private int vertexIndex;
+
+ private int version = -1;
+ private int numberElements = 0;
+
+ public VertexAttributeBinding(VertexResource resource, int offset, VertexAttribute attribute) {
+ this.resource = resource;
+ this.attribute = attribute;
+ this.offset = offset;
+ }
+
+ public VertexResource getResource() {
+ return resource;
+ }
+
+ public void reserve(int vertCount) {
+ resource.ensureCapacity(attribute.configuration.size(vertCount, this.offset, resource));
+ }
+
+ public void allocate(int elements) {
+ this.resource.reserveElements(elements);
+ }
+
+ public void rewind() {
+ this.vertexIndex = 0;
+ }
+
+ public void setPosition(int index) {
+ this.vertexIndex = index;
+ }
+
+ public int numberOfElements() {
+ if (version != resource.getVersion()) {
+ update();
+ }
+ return numberElements;
+ }
+
+ private void update() {
+ if (resource.getInSize() == 0 || resource.getInStride() == 0) {
+ numberElements = 0;
+ } else {
+ numberElements = attribute.configuration.numElements(offset, resource);
+ }
+ this.version = resource.getVersion();
+ }
+
+ /**
+ * write a value by the index.
+ *
+ * @param value the value to commit
+ */
+ public void put(T value) {
+ resource.ensureCapacity(attribute.configuration.size(this.vertexIndex, this.offset, resource));
+ attribute.configuration.write(value, this.vertexIndex, this.offset, resource);
+ this.vertexIndex++;
+ this.resource.mark();
+ }
+
+ public void set(int index, T value) {
+ attribute.configuration.write(value, index, this.offset, resource);
+ this.resource.mark();
+ }
+
+ public TImpl get(int index, TImpl dest) {
+ return attribute.configuration.read(index, this.offset, resource, dest);
+ }
+}
diff --git a/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/resource/VertexResource.java b/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/resource/VertexResource.java
new file mode 100644
index 00000000..4d3aceba
--- /dev/null
+++ b/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/resource/VertexResource.java
@@ -0,0 +1,135 @@
+// Copyright 2021 The Terasology Foundation
+// SPDX-License-Identifier: Apache-2.0
+
+package org.terasology.gestalt.graphics.resource;
+
+import org.lwjgl.BufferUtils;
+
+import java.nio.ByteBuffer;
+
+public class VertexResource {
+ private int inStride = 0;
+ private int inSize = 0;
+ private int version = 0;
+ private ByteBuffer buffer = BufferUtils.createByteBuffer(0);
+ private VertexDefinition[] attributes;
+
+ public int inSize() {
+ return inSize;
+ }
+
+ public VertexResource() {
+
+ }
+
+ public VertexDefinition[] definitions() {
+ return this.attributes;
+ }
+
+ public ByteBuffer buffer() {
+ return this.buffer;
+ }
+
+ public void setDefinitions(VertexDefinition[] attr) {
+ this.attributes = attr;
+ }
+
+ public VertexResource(int inSize, int inStride, VertexDefinition[] attributes) {
+ this.inStride = inStride;
+ this.inSize = inSize;
+ this.attributes = attributes;
+ this.buffer = BufferUtils.createByteBuffer(inSize);
+ }
+
+ public void copy(VertexResource resource) {
+ if (resource.inSize == 0) {
+ return;
+ }
+ ensureCapacity(resource.inSize);
+ ByteBuffer copyBuffer = resource.buffer;
+ copyBuffer.limit(resource.inSize());
+ copyBuffer.rewind();
+ buffer.put(copyBuffer);
+
+ this.inSize = resource.inSize;
+ this.inStride = resource.inStride;
+ this.mark();
+ }
+
+ public int getInSize() {
+ return inSize;
+ }
+
+ public int getInStride() {
+ return inStride;
+ }
+
+ public void ensureCapacity(int size) {
+ if (size > buffer.capacity()) {
+ int newCap = Math.max(this.inSize << 1, size);
+ ByteBuffer newBuffer = BufferUtils.createByteBuffer(newCap);
+ buffer.limit(Math.min(size, this.inSize));
+ buffer.position(0);
+ newBuffer.put(buffer);
+ this.buffer = newBuffer;
+ }
+ if (size > this.inSize) {
+ this.inSize = size;
+ }
+ }
+
+ public void reserveElements(int verts) {
+ int size = verts * inStride;
+ ensureCapacity(size);
+ }
+
+ public void reallocateElements(int verts) {
+ int size = verts * inStride;
+ ensureCapacity(size);
+ this.inSize = size;
+ squeeze();
+
+ }
+
+ public void squeeze() {
+ if (this.inSize != buffer.capacity()) {
+ ByteBuffer newBuffer = BufferUtils.createByteBuffer(this.inSize);
+ buffer.limit(this.inSize);
+ buffer.position(0);
+ newBuffer.put(buffer);
+ this.buffer = newBuffer;
+ }
+ }
+
+ public void allocate(int size, int stride) {
+ this.ensureCapacity(size);
+ this.inStride = stride;
+ this.inSize = size;
+ }
+
+ public int getVersion() {
+ return version;
+ }
+
+ /**
+ * increase version flag for change
+ */
+ public void mark() {
+ version++;
+ }
+
+ /**
+ * describes the metadata and placement into the buffer based off the stride.
+ */
+ public static class VertexDefinition {
+ public final int location;
+ public final VertexAttribute attribute;
+ public final int offset;
+
+ public VertexDefinition(int location, int offset, VertexAttribute attribute) {
+ this.location = location;
+ this.attribute = attribute;
+ this.offset = offset;
+ }
+ }
+}
diff --git a/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/resource/VertexResourceBuilder.java b/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/resource/VertexResourceBuilder.java
new file mode 100644
index 00000000..a5877666
--- /dev/null
+++ b/gestalt-graphics-core/src/main/java/org/terasology/gestalt/graphics/resource/VertexResourceBuilder.java
@@ -0,0 +1,37 @@
+// Copyright 2021 The Terasology Foundation
+// SPDX-License-Identifier: Apache-2.0
+
+package org.terasology.gestalt.graphics.resource;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class VertexResourceBuilder {
+ private List definitions = new ArrayList<>();
+ private int inStride;
+ private VertexResource resource = new VertexResource();
+
+ public VertexResourceBuilder() {
+ }
+
+ /**
+ * add an attribute and provides an {@link VertexAttributeBinding}
+ *
+ * @param location the index of the attribute binding
+ * @param attribute the attribute that describes the binding
+ * @param
+ * @return
+ */
+ public VertexAttributeBinding add(int location, VertexAttribute attribute) {
+ VertexAttributeBinding result = new VertexAttributeBinding(resource, inStride, attribute);
+ this.definitions.add(new VertexResource.VertexDefinition(location, inStride, attribute));
+ inStride += attribute.mapping.size * attribute.count;
+ return result;
+ }
+
+ public VertexResource build() {
+ resource.setDefinitions(definitions.toArray(new VertexResource.VertexDefinition[]{}));
+ resource.allocate(0, inStride);
+ return resource;
+ }
+}
diff --git a/gestalt-graphics-core/src/test/java/org/terasology/gestalt/graphics/ColorTest.java b/gestalt-graphics-core/src/test/java/org/terasology/gestalt/graphics/ColorTest.java
new file mode 100644
index 00000000..8f949915
--- /dev/null
+++ b/gestalt-graphics-core/src/test/java/org/terasology/gestalt/graphics/ColorTest.java
@@ -0,0 +1,296 @@
+package org.terasology.gestalt.graphics;
+
+import org.joml.Vector3f;
+import org.joml.Vector4f;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ColorTest {
+
+ static Stream singleColorChangeIntArgs() {
+ return Stream.of(
+ Arguments.of(255, 255),
+ Arguments.of(175, 175),
+ Arguments.of(125, 125),
+ Arguments.of(600, 255),
+ Arguments.of(0, 0)
+ );
+ };
+
+ static Stream setColorVector4fArgs() {
+ return Stream.of(
+ Arguments.of(new Vector4f(1.0f,0,0,0), new Color(255, 0,0,0)),
+ Arguments.of(new Vector4f(0,1.0f,0,0), new Color(0, 255,0,0)),
+ Arguments.of(new Vector4f(0,0,1.0f,0.0f), new Color(0, 0,255,0)),
+ Arguments.of(new Vector4f(0,0,0.0f,1.0f), new Color(0, 0,0,255))
+ );
+ }
+
+ static Stream setColorVector3fArgs() {
+ return Stream.of(
+ Arguments.of(new Vector3f(1.0f,0,0), new Color(255, 0,0,255)),
+ Arguments.of(new Vector3f(0,1.0f,0), new Color(0, 255,0,255)),
+ Arguments.of(new Vector3f(0,0,1.0f), new Color(0, 0,255,255))
+ );
+ }
+
+ static Stream SingleColorChangeFloatArgs() {
+ return Stream.of(
+ Arguments.of(1.0f, 255),
+ Arguments.of(0.68f, 173),
+ Arguments.of(0.49f, 124),
+ Arguments.of(6.0f, 255),
+ Arguments.of(-1.0f, 0),
+ Arguments.of(0, 0)
+ );
+ };
+
+ static Stream inverseColorArgs() {
+ return Stream.of(
+ Arguments.of(new Color(255, 0, 0, 255), new Color(0, 255, 255, 255)),
+ Arguments.of(new Color(0, 125, 0, 255), new Color(255, 130, 255, 255)),
+ Arguments.of(new Color(125, 125, 0, 255), new Color(130, 130, 255, 255)),
+ Arguments.of(new Color(0, 0, 90, 255), new Color(255, 255, 165, 255))
+ );
+ };
+
+ static Stream hexColorArgs() {
+ return Stream.of(
+ Arguments.of(new Color(255, 0, 0, 255),"FF0000FF"),
+ Arguments.of(new Color(0, 125, 0, 255), "007D00FF"),
+ Arguments.of(new Color(125, 125, 0, 255), "7D7D00FF"),
+ Arguments.of(new Color(0, 0, 90, 255), "00005AFF")
+ );
+ };
+
+
+ @ParameterizedTest
+ @MethodSource("hexColorArgs")
+ public void testHexColor(Color c, String expected) {
+ Color c1 = new Color(c);
+ assertEquals(c1.toHex(), expected);
+ }
+
+
+ @ParameterizedTest
+ @MethodSource("singleColorChangeIntArgs")
+ public void testSetColorR(int test, int expected) {
+ Color c1 = new Color();
+ c1.setRed(test);
+
+ assertEquals(expected, c1.r());
+ assertEquals(0, c1.g());
+ assertEquals(0, c1.b());
+ assertEquals(255, c1.a());
+ }
+
+
+ @ParameterizedTest
+ @MethodSource("singleColorChangeIntArgs")
+ public void testSetColorG(int value, int expected) {
+ Color c1 = new Color();
+ c1.setGreen(value);
+
+ assertEquals(0, c1.r());
+ assertEquals(expected, c1.g());
+ assertEquals(0, c1.b());
+ assertEquals(255, c1.a());
+ }
+
+ @ParameterizedTest
+ @MethodSource("singleColorChangeIntArgs")
+ public void testSetColorB(int value, int expected) {
+ Color c1 = new Color();
+ c1.setBlue(value);
+
+ assertEquals(0, c1.r());
+ assertEquals(0, c1.g());
+ assertEquals(expected, c1.b());
+ assertEquals(255, c1.a());
+
+ }
+
+ @ParameterizedTest
+ @MethodSource("singleColorChangeIntArgs")
+ public void testSetColorA(int value, int expected) {
+ Color c1 = new Color();
+ c1.setAlpha(value);
+
+ assertEquals(0, c1.r());
+ assertEquals(0, c1.g());
+ assertEquals(0, c1.b());
+ assertEquals(expected, c1.a());
+ }
+
+ @ParameterizedTest
+ @MethodSource("singleColorChangeIntArgs")
+ public void testIntColorR(int value, int expected) {
+ Color c1 = new Color(value, 0, 0);
+
+ assertEquals(expected, c1.r());
+ assertEquals(0, c1.g());
+ assertEquals(0, c1.b());
+ assertEquals(255, c1.a());
+ }
+
+ @ParameterizedTest
+ @MethodSource("singleColorChangeIntArgs")
+ public void testIntColorG(int value, int expected) {
+ Color c2 = new Color(0, value, 0);
+
+ assertEquals(c2.r(), 0);
+ assertEquals(c2.g(), expected);
+ assertEquals(c2.b(), 0);
+ assertEquals(c2.a(), 255);
+ }
+
+ @ParameterizedTest
+ @MethodSource("singleColorChangeIntArgs")
+ public void testIntColorB(int value, int expected) {
+ Color c3 = new Color(0, 0, value);
+
+ assertEquals(c3.r(), 0);
+ assertEquals(c3.g(), 0);
+ assertEquals(c3.b(), expected);
+ assertEquals(c3.a(), 255);
+ }
+
+ @ParameterizedTest
+ @MethodSource("singleColorChangeIntArgs")
+ public void testIntColorA(int value, int expected) {
+ Color c3 = new Color(0, 0, 0,value);
+
+ assertEquals(c3.r(), 0);
+ assertEquals(c3.g(), 0);
+ assertEquals(c3.b(), 0);
+ assertEquals(c3.a(), expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("SingleColorChangeFloatArgs")
+ public void testFloatColorR(float value, int expected) {
+ Color c2 = new Color(value, 0, 0);
+
+ assertEquals(c2.r(), expected);
+ assertEquals(c2.g(), 0);
+ assertEquals(c2.b(), 0);
+ assertEquals(c2.a(), 255);
+ }
+
+ @ParameterizedTest
+ @MethodSource("SingleColorChangeFloatArgs")
+ public void testFloatColorG(float value, int expected) {
+ Color c2 = new Color(0, value, 0);
+
+ assertEquals(c2.r(), 0);
+ assertEquals(c2.g(), expected);
+ assertEquals(c2.b(), 0);
+ assertEquals(c2.a(), 255);
+ }
+
+ @ParameterizedTest
+ @MethodSource("SingleColorChangeFloatArgs")
+ public void testFloatColorB(float value, int expected) {
+ Color c2 = new Color(0, 0.0f, value);
+
+ assertEquals(c2.r(), 0);
+ assertEquals(c2.g(), 0);
+ assertEquals(c2.b(), expected);
+ assertEquals(c2.a(), 255);
+ }
+
+
+ @ParameterizedTest
+ @MethodSource("SingleColorChangeFloatArgs")
+ public void testSetFloatColorA(float value, int expected) {
+ Color test = new Color();
+ test.setAlpha(value);
+
+ assertEquals(test.r(), 0);
+ assertEquals(test.g(), 0);
+ assertEquals(test.b(), 0);
+ assertEquals(test.a(), expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("SingleColorChangeFloatArgs")
+ public void testSetFloatColorR(float value, int expected) {
+ Color test = new Color();
+ test.setRed(value);
+
+ assertEquals(test.r(), expected);
+ assertEquals(test.g(), 0);
+ assertEquals(test.b(), 0);
+ assertEquals(test.a(), 255);
+ }
+
+ @ParameterizedTest
+ @MethodSource("SingleColorChangeFloatArgs")
+ public void testSetFloatColorG(float value, int expected) {
+ Color c2 = new Color();
+ c2.setGreen(value);
+
+ assertEquals(c2.r(), 0);
+ assertEquals(c2.g(), expected);
+ assertEquals(c2.b(), 0);
+ assertEquals(c2.a(), 255);
+ }
+
+ @ParameterizedTest
+ @MethodSource("SingleColorChangeFloatArgs")
+ public void testSetFloatColorB(float value, int expected) {
+ Color c2 = new Color();
+ c2.setBlue(value);
+
+ assertEquals(c2.r(), 0);
+ assertEquals(c2.g(), 0);
+ assertEquals(c2.b(), expected);
+ assertEquals(c2.a(), 255);
+ }
+
+
+ @ParameterizedTest
+ @MethodSource("SingleColorChangeFloatArgs")
+ public void testFloatColorA(float value, int expected) {
+ Color test = new Color();
+ test.setAlpha(value);
+
+ assertEquals(test.r(), 0);
+ assertEquals(test.g(), 0);
+ assertEquals(test.b(), 0);
+ assertEquals(test.a(), expected);
+ }
+
+
+
+ @ParameterizedTest
+ @MethodSource("inverseColorArgs")
+ public void testInvert(Color value, Color expected) {
+ Color test = new Color(value);
+ test.invert();
+ assertEquals(expected, test);
+ }
+
+
+ @ParameterizedTest
+ @MethodSource("setColorVector4fArgs")
+ public void testSetColorVector4f(Vector4f pos, Color expected) {
+ Color test = new Color();
+ test.set(pos);
+ assertEquals(test, expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("setColorVector3fArgs")
+ public void testSetColorVector3f(Vector3f pos, Color expected) {
+ Color c = new Color();
+ c.set(pos);
+ assertEquals(c, expected);
+ }
+}
+
diff --git a/settings.gradle b/settings.gradle
index dbcf025b..b841ed1b 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,5 +1,5 @@
rootProject.name = 'gestalt'
-include 'gestalt-util', 'gestalt-di', 'gestalt-inject-java', 'gestalt-inject', 'gestalt-annotation', 'testpack:testpack-api', 'gestalt-module', 'testpack:moduleA', 'testpack:moduleB', 'testpack:moduleC', 'testpack:moduleD', 'testpack:moduleF', 'gestalt-asset-core', 'gestalt-entity-system', 'gestalt-es-perf'
+include 'gestalt-graphics-core', 'gestalt-util', 'gestalt-di', 'gestalt-inject-java', 'gestalt-inject', 'gestalt-annotation', 'testpack:testpack-api', 'gestalt-module', 'testpack:moduleA', 'testpack:moduleB', 'testpack:moduleC', 'testpack:moduleD', 'testpack:moduleF', 'gestalt-asset-core', 'gestalt-entity-system', 'gestalt-es-perf'
if (rootProject.projectDir.toPath().resolve("local.properties").toFile().exists()) {
include 'gestalt-android', 'gestalt-android-testbed'
} else {