diff --git a/gradleScripts/dependenciesCommon.gradle b/gradleScripts/dependenciesCommon.gradle
index e724193..f6515e0 100644
--- a/gradleScripts/dependenciesCommon.gradle
+++ b/gradleScripts/dependenciesCommon.gradle
@@ -11,5 +11,6 @@ dependencies {
     api "org.fusesource.jansi:jansi:${jansiVersion}"
     api "org.joml:joml:${jomlVersion}"
     api "com.electronwill.night-config:toml:${nightConfigVersion}"
+    api "com.electronwill.night-config:json:${nightConfigVersion}"
     api "com.github.zafarkhaja:java-semver:${jsemverVersion}"
 }
\ No newline at end of file
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/RenderGraph.java b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/RenderGraph.java
index 375893e..9050dad 100644
--- a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/RenderGraph.java
+++ b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/RenderGraph.java
@@ -10,7 +10,7 @@
 import com.terminalvelocitycabbage.engine.registry.Identifier;
 import com.terminalvelocitycabbage.engine.util.ClassUtils;
 import com.terminalvelocitycabbage.engine.util.Toggle;
-import com.terminalvelocitycabbage.engine.util.touples.Pair;
+import com.terminalvelocitycabbage.engine.util.tuples.Pair;
 import com.terminalvelocitycabbage.templates.events.RenderGraphStageExecutionEvent;
 
 import javax.management.ReflectionException;
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/animation/Animation.java b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/animation/Animation.java
new file mode 100644
index 0000000..28dcb61
--- /dev/null
+++ b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/animation/Animation.java
@@ -0,0 +1,179 @@
+package com.terminalvelocitycabbage.engine.client.renderer.animation;
+
+import com.terminalvelocitycabbage.engine.util.Easing;
+import com.terminalvelocitycabbage.engine.util.tuples.Pair;
+import com.terminalvelocitycabbage.engine.util.tuples.Triplet;
+import org.joml.Vector3f;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class Animation {
+
+    long time; //The time that this animation has been playing
+    long currentTime;
+    final long startDelay;
+    final long animationLength;
+    final long loopDelay;
+    final long loopLength;
+
+    //Status
+    boolean isPlaying;
+    int playsRemaining; //-1 if indefinite
+    boolean startFromZero;
+
+    //Bone name, keyframe
+    private final Map<String, List<Keyframe>> keyframes;
+
+    public Animation(long startDelay, long animationLength, long loopDelay, Map<String, List<Keyframe>> keyframes) {
+        this.time = 0;
+        this.currentTime = 0;
+        this.startDelay = startDelay;
+        this.animationLength = animationLength;
+        this.loopDelay = loopDelay;
+        this.loopLength = animationLength + loopDelay;
+        this.isPlaying = false;
+        this.playsRemaining = 0;
+        this.startFromZero = false;
+        this.keyframes = keyframes;
+    }
+
+    public void update(long deltaTime) {
+
+        //Get the current position in the looping keyframe
+        if (isPlaying) {
+
+            time = startFromZero ? 0 : time + deltaTime;
+            currentTime = time - startDelay;
+
+            if (playsRemaining != -1 && currentTime > animationLength) {
+                playsRemaining = Math.max(playsRemaining - 1, 0);
+            }
+
+            if (playsRemaining == 0) {
+                currentTime = animationLength + startDelay;
+            } else {
+                currentTime %= loopLength;
+            }
+        }
+
+        startFromZero = false;
+    }
+
+    public void play() {
+        play(true);
+    }
+
+    public void play(boolean startOver) {
+        this.startFromZero = startOver;
+        isPlaying = true;
+        playsRemaining = 1;
+    }
+
+    public void repeat(int timesToRepeat) {
+        isPlaying = true;
+        playsRemaining = timesToRepeat;
+    }
+
+    public void loop() {
+        isPlaying = true;
+        playsRemaining = -1;
+    }
+
+    public void pause() {
+        isPlaying = false;
+    }
+
+    public void resume() {
+        isPlaying = true;
+    }
+
+    public void stop() {
+        isPlaying = false;
+        playsRemaining = 0;
+        time = 0;
+        currentTime = 0;
+    }
+
+    //Percentage through keyframe, the target transformation
+    public List<Keyframe> getCurrentKeyframes(String boneName) {
+
+        List<Keyframe> currentKeyFrames = new ArrayList<>();
+
+        for (Keyframe keyframe : keyframes.get(boneName)) {
+            if (currentTime > keyframe.startTime && currentTime < keyframe.endTime) currentKeyFrames.add(keyframe);
+        }
+
+        return currentKeyFrames;
+    }
+
+    //Bone name, transformations
+    Map<String, TransformationSnapshot> currentTransformations = new HashMap<>();
+    List<Pair<Float, Keyframe>> progressKeyframes = new ArrayList<>();
+    public Map<String, TransformationSnapshot> getCurrentTransformations() {
+        currentTransformations.clear();
+        progressKeyframes.clear();
+        for (String boneName : keyframes.keySet()) {
+            for (Keyframe keyframe : getCurrentKeyframes(boneName)) {
+                progressKeyframes.add(new Pair<>(getKeyframeProgress(keyframe), keyframe));
+            }
+            currentTransformations.put(boneName, getCurrentTransforms(progressKeyframes));
+        }
+
+        return currentTransformations;
+    }
+
+    /**
+     * Return the value from 0 to 1 that defines the progress between KS and KE for this animation
+     *
+     * AS = animation start
+     * KS = keyframe start time
+     * CT = currentTime
+     * KE = keyframe end time
+     *     AS          KS   CT KE
+     *     |-----------|----x--|-----------...
+     *
+     * to make the math easier we should just subtract everything by KS
+     * KS = KS - KS (0)
+     * CT = CT - KS
+     * KE = KE - AS
+     *
+     * percentage is just CT / KE so
+     * (CT - KS) / (KE - AS) = percentage
+     *
+     * @param keyframe The keyframe we want the progress of
+     * @return the progress from 0 to 1 of this keyframe
+     */
+    float getKeyframeProgress(Keyframe keyframe) {
+        return (currentTime - keyframe.startTime) / (keyframe.endTime - keyframe.startTime);
+    }
+
+    TransformationSnapshot getCurrentTransforms(List<Pair<Float, Keyframe>> keyframes) {
+
+        //combine all keyframe transformations into one
+        Vector3f position = new Vector3f();
+        Vector3f rotation = new Vector3f();
+        Vector3f scale = new Vector3f(1);
+
+        for (Pair<Float, Keyframe> entry : keyframes) {
+            float progress = entry.getValue0();
+            Keyframe keyframe = entry.getValue1();
+            switch (keyframe.component) {
+                case POSITION -> interpolateComponent(position, progress, keyframe);
+                case ROTATION -> interpolateComponent(rotation, progress, keyframe);
+                case SCALE -> interpolateComponent(scale, progress, keyframe);
+            }
+        }
+
+        return new TransformationSnapshot(position, rotation, scale);
+    }
+
+    void interpolateComponent(Vector3f transformation, float progress, Keyframe keyframe) {
+        var xTransform = keyframe.endTransformation.x() == 0 ? keyframe.startTransformation.x() : Easing.easeInOut(keyframe.easingFunction, progress) * (keyframe.endTransformation.x() - keyframe.startTransformation.x());
+        var yTransform = keyframe.endTransformation.y() == 0 ? keyframe.startTransformation.y() : Easing.easeInOut(keyframe.easingFunction, progress) * (keyframe.endTransformation.y() - keyframe.startTransformation.y());
+        var zTransform = keyframe.endTransformation.z() == 0 ? keyframe.startTransformation.z() : Easing.easeInOut(keyframe.easingFunction, progress) * (keyframe.endTransformation.z() - keyframe.startTransformation.z());
+        transformation.add(xTransform, yTransform, zTransform);
+    }
+}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/animation/AnimationController.java b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/animation/AnimationController.java
new file mode 100644
index 0000000..0cb8ddc
--- /dev/null
+++ b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/animation/AnimationController.java
@@ -0,0 +1,88 @@
+package com.terminalvelocitycabbage.engine.client.renderer.animation;
+
+import com.terminalvelocitycabbage.engine.client.renderer.model.Model;
+import org.joml.Matrix4f;
+import org.joml.Quaternionf;
+import org.joml.Vector3f;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class AnimationController {
+
+    //TODO replace string with identifier once animation registry exists
+    private final Map<String, Animation> animations;
+    private final Map<String, Model.Bone> bonesMap;
+    private final Map<Integer, Model.Bone> boneIndexMap;
+    private final Map<Integer, TransformationSnapshot> boneTransformations;
+    private final Map<Integer, Matrix4f> boneTransformationMatrices;
+
+    public AnimationController(Map<String, Animation> animations, Map<String, Model.Bone> bonesMap) {
+        this.animations = animations;
+        this.bonesMap = bonesMap;
+        boneIndexMap = new HashMap<>();
+        boneTransformations = new HashMap<>();
+        boneTransformationMatrices = new HashMap<>();
+        bonesMap.values().forEach(bone -> {
+            boneIndexMap.put(bone.getBoneIndex(), bone);
+            boneTransformations.put(bone.getBoneIndex(), new TransformationSnapshot(new Vector3f(), new Vector3f(), new Vector3f(1)));
+            boneTransformationMatrices.put(bone.getBoneIndex(), new Matrix4f());
+        });
+    }
+
+    public void update(long deltaTime, Model model) {
+        //Reset all transformations from last frame
+        boneTransformations.values().forEach(transformationSnapshot -> {
+            transformationSnapshot.position().zero();
+            transformationSnapshot.rotation().zero();
+            transformationSnapshot.scale().set(1);
+        });
+        boneTransformationMatrices.values().forEach(Matrix4f::identity);
+
+        //Loop through all animations update them and get their contribution to the bone transformations
+        for (Animation animation : animations.values()) {
+            animation.update(deltaTime);
+            //Get this animation's transformations add them together
+            animation.getCurrentTransformations().forEach(
+                    (boneName, boneTransformation) -> {
+                        var transformation = boneTransformations.get(bonesMap.get(boneName).getBoneIndex());
+                        transformation.position().add(boneTransformation.position());
+                        transformation.rotation().add(boneTransformation.rotation());
+                        transformation.scale().mul(boneTransformation.scale());
+                    }
+            );
+        }
+        //Convert all of these updated and combined transformations into a single transformation matrix for each bone
+        for (Map.Entry<Integer, Model.Bone> entry : boneIndexMap.entrySet()) {
+
+            var index = entry.getKey();
+            var bone = entry.getValue();
+
+            var boneTransformation = boneTransformations.get(index);
+            var eulerRotation = boneTransformation.rotation();
+            var rotation = new Quaternionf().rotateXYZ(
+                    (float) Math.toRadians(eulerRotation.x),
+                    (float) Math.toRadians(eulerRotation.y),
+                    (float) Math.toRadians(eulerRotation.z)
+            );
+            boneTransformationMatrices.get(index)
+                    .identity()
+                    .scale(boneTransformation.scale())
+                    .rotateAroundLocal(rotation, bone.getPivotPoint().x, bone.getPivotPoint().y, bone.getPivotPoint().z)
+                    .translate(boneTransformation.position());
+                    //.translate(bone.getOffset());
+        }
+    }
+
+    public Animation getAnimation(String animationName) {
+        return animations.get(animationName);
+    }
+
+    public void stopAll() {
+        animations.values().forEach(Animation::stop);
+    }
+
+    public Map<Integer, Matrix4f> getBoneTransformations() {
+        return boneTransformationMatrices;
+    }
+}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/animation/Keyframe.java b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/animation/Keyframe.java
new file mode 100644
index 0000000..991d30d
--- /dev/null
+++ b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/animation/Keyframe.java
@@ -0,0 +1,68 @@
+package com.terminalvelocitycabbage.engine.client.renderer.animation;
+
+import com.terminalvelocitycabbage.engine.util.Easing;
+import org.joml.Vector3f;
+
+/**
+ * Represents a portion of a transformation between two target transformations of a single component.
+ */
+public class Keyframe {
+
+    Component component;
+    Vector3f startTransformation;
+    Vector3f endTransformation;
+    Easing.Function easingFunction;
+    float startTime;
+    float endTime;
+
+    public Keyframe(Component component, Vector3f startTransformation, Vector3f endTransformation, Easing.Function easingFunction, float startTime, float endTime) {
+        this.component = component;
+        this.startTransformation = startTransformation;
+        this.endTransformation = endTransformation;
+        this.easingFunction = easingFunction;
+        this.startTime = startTime;
+        this.endTime = endTime;
+    }
+
+    public Component getComponent() {
+        return component;
+    }
+
+    public Vector3f getStartTransformation() {
+        return startTransformation;
+    }
+
+    public Vector3f getEndTransformation() {
+        return endTransformation;
+    }
+
+    public Easing.Function getEasingFunction() {
+        return easingFunction;
+    }
+
+    public float getStartTime() {
+        return startTime;
+    }
+
+    public float getEndTime() {
+        return endTime;
+    }
+
+    @Override
+    public String toString() {
+        return "Keyframe{" +
+                "component=" + component +
+                ", startTransformation=" + startTransformation +
+                ", endTransformation=" + endTransformation +
+                ", easingFunction=" + easingFunction +
+                ", startTime=" + startTime +
+                ", endTime=" + endTime +
+                '}';
+    }
+
+    public enum Component {
+        POSITION,
+        ROTATION,
+        SCALE
+    }
+}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/animation/TransformationSnapshot.java b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/animation/TransformationSnapshot.java
new file mode 100644
index 0000000..8c19250
--- /dev/null
+++ b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/animation/TransformationSnapshot.java
@@ -0,0 +1,6 @@
+package com.terminalvelocitycabbage.engine.client.renderer.animation;
+
+import org.joml.Vector3f;
+
+public record TransformationSnapshot(Vector3f position, Vector3f rotation, Vector3f scale) {
+}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/animation/bedrock/BedrockAnimationData.java b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/animation/bedrock/BedrockAnimationData.java
new file mode 100644
index 0000000..5edc7b9
--- /dev/null
+++ b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/animation/bedrock/BedrockAnimationData.java
@@ -0,0 +1,212 @@
+package com.terminalvelocitycabbage.engine.client.renderer.animation.bedrock;
+
+import com.electronwill.nightconfig.core.Config;
+import com.electronwill.nightconfig.core.ConfigFormat;
+import com.electronwill.nightconfig.core.io.ConfigParser;
+import com.electronwill.nightconfig.json.JsonFormat;
+import com.github.zafarkhaja.semver.Version;
+import com.terminalvelocitycabbage.engine.client.renderer.animation.Animation;
+import com.terminalvelocitycabbage.engine.client.renderer.animation.AnimationController;
+import com.terminalvelocitycabbage.engine.client.renderer.animation.Keyframe;
+import com.terminalvelocitycabbage.engine.client.renderer.model.Model;
+import com.terminalvelocitycabbage.engine.debug.Log;
+import com.terminalvelocitycabbage.engine.filesystem.resources.Resource;
+import com.terminalvelocitycabbage.engine.util.ConfigUtils;
+import com.terminalvelocitycabbage.engine.util.Easing;
+import org.joml.Vector3f;
+
+import java.util.*;
+
+public class BedrockAnimationData {
+
+    Version formatVersion;
+    //Animation name, Animation data
+    Map<String, AnimationData> animations;
+
+    public BedrockAnimationData(Version formatVersion, Map<String, AnimationData> animations) {
+        this.formatVersion = formatVersion;
+        this.animations = animations;
+    }
+
+    public void print() {
+        Log.info(formatVersion);
+        for (Map.Entry<String, AnimationData> entry : animations.entrySet()) {
+            Log.info(entry.getKey() + " " + entry.getValue());
+        }
+    }
+
+    public AnimationController toAnimationController(Model model) {
+
+        Map<String, Animation> convertedAnimations = new HashMap<>();
+        Map<String, List<Keyframe>> keyframes;
+        for (Map.Entry<String, AnimationData> entry : animations.entrySet()) {
+            var animationName = entry.getKey();
+            var data = entry.getValue();
+
+            //Convert bedrock transformation data into keyframes
+            keyframes = new HashMap<>();
+
+            for (Map.Entry<String, List<Keyframe>> boneData : data.boneKeyframes.entrySet()) {
+
+                var boneName = boneData.getKey();
+                var boneKeyframes = boneData.getValue();
+
+                keyframes.put(boneName, boneKeyframes);
+            }
+
+            Animation animation = new Animation(
+                    Math.round(data.startDelay * 1000f),
+                    Math.round(data.animationLength * 1000f),
+                    Math.round(data.loopDelay * 1000f),
+                    keyframes
+
+            );
+
+            convertedAnimations.put(animationName, animation);
+        }
+
+        return new AnimationController(convertedAnimations, model.getBones());
+    }
+
+    public record AnimationData (
+        boolean loop,
+        float animationLength,
+        float startDelay,
+        float loopDelay,
+        //Bone name, keyframes
+        Map<String, List<Keyframe>> boneKeyframes
+    ) {
+
+        @Override
+        public String toString() {
+            return "AnimationData{" +
+                    "loop=" + loop +
+                    ", animationLength=" + animationLength +
+                    ", startDelay=" + startDelay +
+                    ", loopDelay=" + loopDelay +
+                    ", boneKeyframes=" + boneKeyframes +
+                    '}';
+        }
+    }
+
+    public static class Loader {
+
+        static Version formatVersion;
+        static Map<String, AnimationData> animations = new HashMap<>();
+
+        public static BedrockAnimationData loadAnimations(Resource animationsResource) {
+
+            String resourceString = animationsResource.asString();
+
+            //Not sure where best to have this call, but it is needed to be called sometime before reading animations.
+            Config.setInsertionOrderPreserved(true);
+            ConfigFormat<?> jsonFormat = JsonFormat.fancyInstance();
+            ConfigParser<?> parser = jsonFormat.createParser();
+            Config config = parser.parse(resourceString);
+
+            formatVersion = Version.parse(config.get("format_version"));
+
+            Config animationsConfig = config.get("animations");
+            animationsConfig.valueMap().forEach((key, value) -> parseAnimationConfig(key, (Config) value, animations));
+
+            return new BedrockAnimationData(formatVersion, animations);
+        }
+
+        private static void parseAnimationConfig(String animationName, Config animation, Map<String, AnimationData> animationData) {
+
+            var animationLength = Float.parseFloat(animation.get("animation_length").toString());
+            AnimationData data = new AnimationData(
+                    animation.get("loop"),
+                    animationLength,
+                    Float.parseFloat(animation.get("start_delay").toString()),
+                    Float.parseFloat(animation.get("loop_delay").toString()),
+                    parseBedrockBoneData(animation.get("bones"), animationLength)
+            );
+
+            animationData.put(animationName, data);
+        }
+
+        //bone name, Position, Rotation, Scale
+        private static Map<String, List<Keyframe>> parseBedrockBoneData(Config bones, float animationLength) {
+
+            Map<String, List<Keyframe>> boneKeyframeMap = new HashMap<>();
+
+            bones.valueMap().forEach((key, value) -> {
+
+                var bonesConfig = (Config) value;
+                List<Keyframe> keyframes = new ArrayList<>();
+
+                keyframes.addAll(parseTransformationData(Keyframe.Component.POSITION, bonesConfig.get("position"), animationLength));
+                keyframes.addAll(parseTransformationData(Keyframe.Component.ROTATION, bonesConfig.get("rotation"), animationLength));
+                keyframes.addAll(parseTransformationData(Keyframe.Component.SCALE, bonesConfig.get("scale"), animationLength));
+
+                boneKeyframeMap.put(key, keyframes);
+            });
+
+            return boneKeyframeMap;
+        }
+
+        //TODO parse molang
+        private static List<Keyframe> parseTransformationData(Keyframe.Component component, Config transformationConfig, float endTime) {
+
+            if (transformationConfig == null) return Collections.emptyList();
+
+            List<Keyframe> keyframes = new ArrayList<>();
+            int iteration = 0;
+            for (Map.Entry<String, Object> entry : transformationConfig.valueMap().entrySet()) {
+                //Easier entry names
+                String endTimeSeconds = entry.getKey();
+                Object keyframeConfigOrTransformation = entry.getValue();
+
+                //The previous keyframes final transformation to use used as this keyframe's start transformation
+                Vector3f previousKeyframeEndTransformation;
+                //The previous keyframes end time to be used as this keyframes start time
+                float previousKeyframeEndTime;
+                //If this is the first iteration we assume this is the first keyframe
+                if (iteration == 0) {
+                    //Some default values for the first keyframe (this keyframe gets deleted at the end since it's just setup)
+                    previousKeyframeEndTransformation = new Vector3f();
+                    previousKeyframeEndTime = 0f;
+                } else {
+                    //Get the previous keyframe and some info about it to use for this keyframe
+                    var previousKeyframe = keyframes.get(iteration - 1);
+                    previousKeyframeEndTransformation = previousKeyframe.getEndTransformation();
+                    previousKeyframeEndTime = previousKeyframe.getEndTime();
+                }
+                var endTimeMillis = Float.parseFloat(endTimeSeconds) * 1000f;
+
+                //Create the keyframes
+                if (keyframeConfigOrTransformation.toString().startsWith("[")) { //linear is simplified out in bbmodel
+                    keyframes.add(new Keyframe(
+                            component,
+                            previousKeyframeEndTransformation,
+                            ConfigUtils.numberListToVector3f((List<Number>) keyframeConfigOrTransformation),
+                            Easing.Function.LINEAR,
+                            previousKeyframeEndTime,
+                            endTimeMillis
+                    ));
+                } else { //Usually Non-linear interpolation
+                    keyframes.add(new Keyframe(
+                            component,
+                            previousKeyframeEndTransformation,
+                            ConfigUtils.numberListToVector3f(((Config) keyframeConfigOrTransformation).get("post")), //TODO pre
+                            switch (((Config) keyframeConfigOrTransformation).get("lerp_mode").toString()) {
+                                case "catmullrom" -> Easing.Function.CIRCULAR;
+                                case "step" -> Easing.Function.STEP;
+                                default -> Easing.Function.LINEAR;
+                            },
+                            previousKeyframeEndTime,
+                            endTimeMillis
+                    ));
+                }
+                iteration++;
+            }
+            //The first keyframe is really only to set the stage for the second keyframe to start in the right place
+            keyframes.removeFirst();
+
+            return keyframes;
+        }
+
+    }
+
+}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/elements/VertexAttribute.java b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/elements/VertexAttribute.java
index ee4fb2d..2013511 100644
--- a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/elements/VertexAttribute.java
+++ b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/elements/VertexAttribute.java
@@ -6,7 +6,8 @@ public enum VertexAttribute {
     UV("uv", 2, false),
     XYZ_NORMAL("normal", 3, true),
     RGBA_COLOR("color_rgba", 4, false),
-    RGB_COLOR("color_rgb", 3, false);
+    RGB_COLOR("color_rgb", 3, false),
+    BONE_INDEX("bone_index", 1, false);
 
     private final String name;
     private final int components;
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/elements/VertexFormat.java b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/elements/VertexFormat.java
index 92363dc..55004d3 100644
--- a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/elements/VertexFormat.java
+++ b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/elements/VertexFormat.java
@@ -71,6 +71,13 @@ public boolean hasComponent(VertexAttribute vertexAttribute) {
         return attributes.contains(vertexAttribute);
     }
 
+    @Override
+    public String toString() {
+        return "VertexFormat{" +
+                "attributes=" + attributes +
+                '}';
+    }
+
     /**
      * A required utility for building a vertex format, constructs all the strides and internal data for you
      */
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/model/Mesh.java b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/model/Mesh.java
index f7a9995..dadf73d 100644
--- a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/model/Mesh.java
+++ b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/model/Mesh.java
@@ -5,7 +5,7 @@
 import com.terminalvelocitycabbage.engine.debug.Log;
 import com.terminalvelocitycabbage.engine.util.ArrayUtils;
 import org.lwjgl.opengl.GL30;
-import org.lwjgl.system.MemoryStack;
+import org.lwjgl.system.MemoryUtil;
 
 import java.nio.FloatBuffer;
 import java.nio.IntBuffer;
@@ -35,50 +35,104 @@ public Mesh(VertexFormat format, Vertex[] vertices, int[] indices) {
         this.indices = indices;
     }
 
+    public static Mesh of(List<Mesh> meshes) {
+
+        //Verify all meshes have the same vertex format before merging data
+        VertexFormat format1 = null;
+        //While we're at it track vertex and index counts for later use IF it makes it past this point
+        int vertexCount = 0;
+        int indicesCount = 0;
+        for (Mesh mesh : meshes) {
+            if (format1 != null) {
+                if (format1 != mesh.getFormat()) {
+                    Log.crash("Tried to construct a mesh with mismatched formats: " + format1 + ", " + mesh.getFormat());
+                }
+            }
+            vertexCount += mesh.getNumVertices();
+            indicesCount += mesh.getNumIndices();
+            format1 = mesh.getFormat();
+        }
+
+        //Combine all vertex data and index data
+        Vertex[] vertices = new Vertex[vertexCount];
+        int[] indices = new int[indicesCount];
+        int meshIndex = 0;
+        int vertexIndex = 0;
+        int indexOffset = 0;
+        for (Mesh mesh : meshes) {
+            for (int index : mesh.indices) {
+                indices[meshIndex] = index + indexOffset;
+                meshIndex++;
+            }
+            for (Vertex vertex : mesh.vertices) {
+                vertices[vertexIndex] = vertex;
+                vertexIndex++;
+            }
+            indexOffset += mesh.getNumVertices();
+        }
+
+        return new Mesh(format1, vertices, indices);
+    }
+
     /**
      * Initializes this mesh to be rendered. Only needs to be called once
      */
     public void init() {
-        try (MemoryStack stack = MemoryStack.stackPush()) {
-            vboIdList = new ArrayList<>();
-
-            vaoId = glGenVertexArrays();
-            glBindVertexArray(vaoId);
-
-            int vboId;
-
-            //Create attribute vbos and upload the data from the mesh
-            int attributeIndex = 0;
-            //Loop through all the attributes for the format of this mesh
-            for (VertexAttribute attribute : format.getAttributes()) {
-                //Create a vbo for this attribute data
-                vboId = glGenBuffers();
-                vboIdList.add(vboId);
-                //Get the attribute data from this mesh
-                var attributeData = getDataOfType(attribute);
-                FloatBuffer attributeBuffer = stack.callocFloat(attributeData.length);
+
+        //Just in case it makes it to this point (it shouldn't)
+        if (vertices.length == 0) {
+            Log.error("Tried to initialize an empty mesh.");
+            return;
+        }
+
+        vboIdList = new ArrayList<>();
+
+        vaoId = glGenVertexArrays();
+        glBindVertexArray(vaoId);
+
+        int vboId;
+
+        //Create attribute vbos and upload the data from the mesh
+        int attributeIndex = 0;
+        //Loop through all the attributes for the format of this mesh
+        for (VertexAttribute attribute : format.getAttributes()) {
+            //Create a vbo for this attribute data
+            vboId = glGenBuffers();
+            vboIdList.add(vboId);
+            //Get the attribute data from this mesh
+            var attributeData = getDataOfType(attribute);
+            FloatBuffer attributeBuffer = MemoryUtil.memCallocFloat(attributeData.length);
+            try {
                 attributeBuffer.put(0, attributeData);
                 //Upload this data to OpenGL
                 glBindBuffer(GL_ARRAY_BUFFER, vboId);
                 glBufferData(GL_ARRAY_BUFFER, attributeBuffer, GL_STATIC_DRAW);
                 glEnableVertexAttribArray(attributeIndex);
                 glVertexAttribPointer(attributeIndex, attribute.getNumComponents(), GL_FLOAT, attribute.isNormalized(), 0, 0);
-                attributeIndex++;
+            } finally {
+                MemoryUtil.memFree(attributeBuffer);
             }
+            attributeIndex++;
+        }
 
-            //Create the index buffers for mesh rendering
-            vboId = glGenBuffers();
-            //Upload the data to the buffer and opengl
-            vboIdList.add(vboId);
-            IntBuffer indicesBuffer = stack.callocInt(indices.length);
+        //Create the index buffers for mesh rendering
+        vboId = glGenBuffers();
+        //Upload the data to the buffer and opengl
+        vboIdList.add(vboId);
+        IntBuffer indicesBuffer = MemoryUtil.memAllocInt(indices.length);
+        try {
             indicesBuffer.put(0, indices);
             glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboId);
             glBufferData(GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL_STATIC_DRAW);
-
-            //Bind all buffers
-            glBindBuffer(GL_ARRAY_BUFFER, 0);
-            glBindVertexArray(0);
+        } finally {
+            MemoryUtil.memFree(indicesBuffer);
         }
+
+        //Bind all buffers
+        glBindBuffer(GL_ARRAY_BUFFER, 0);
+        glBindVertexArray(0);
+
+        //Mark this mesh as initialized, so we don't have to do this twice
         initialized = true;
     }
 
@@ -86,6 +140,7 @@ public void init() {
      * Renders this mesh
      */
     public void render() {
+        if (vertices.length == 0) return;
         if (!isInitialized()) init();
         glBindVertexArray(getVaoId());
         glDrawElements(GL_TRIANGLES, getNumIndices(), GL_UNSIGNED_INT, 0);
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/model/Model.java b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/model/Model.java
new file mode 100644
index 0000000..03a9f4d
--- /dev/null
+++ b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/model/Model.java
@@ -0,0 +1,96 @@
+package com.terminalvelocitycabbage.engine.client.renderer.model;
+
+import com.terminalvelocitycabbage.engine.client.renderer.elements.VertexFormat;
+import org.joml.Quaternionf;
+import org.joml.Vector3f;
+
+import java.util.Map;
+
+public class Model {
+
+    VertexFormat format;
+    Map<String, Bone> parts;
+    Mesh mesh;
+
+    public Model(VertexFormat format, Map<String, Bone> bones, Mesh mesh) {
+        this.format = format;
+        this.parts = bones;
+        this.parts.values().forEach(bone -> bone.model = this);
+        this.mesh = mesh;
+    }
+
+    public void render() {
+        mesh.render();
+    }
+
+    public void cleanup() {
+        mesh.cleanup();
+    }
+
+    public VertexFormat getFormat() {
+        return format;
+    }
+
+    public Bone getBone(String partName) {
+        return parts.get(partName);
+    }
+
+    public Map<String, Bone> getBones() {
+        return parts;
+    }
+
+    public Mesh getMesh() {
+        return mesh;
+    }
+
+    public static class Bone {
+
+        Model model;
+
+        String name;
+        String parentName;
+        int boneIndex;
+
+        boolean dirty;
+
+        Vector3f pivotPoint;
+        Quaternionf rotation;
+        Vector3f scale;
+
+        public Bone(String name, String parentName, Vector3f pivotPoint, Quaternionf rotation, Vector3f scale, int boneIndex) {
+            this.name = name;
+            this.parentName = parentName;
+
+            this.dirty = true;
+
+            this.pivotPoint = pivotPoint;
+            this.rotation = rotation;
+            this.scale = scale;
+
+            this.boneIndex = boneIndex;
+        }
+
+        public String getParentName() {
+            return parentName;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        //TODO cache this offset so we don't query every frame, do this on model instantiation
+        public Vector3f getPivotPoint() {
+            Vector3f retOffset = new Vector3f(pivotPoint);
+            var parent = model.getBone(parentName);
+            if (parent != null) {
+                retOffset.add(parent.getPivotPoint(), retOffset);
+            }
+            return retOffset;
+        }
+
+        public int getBoneIndex() {
+            return boneIndex;
+        }
+    }
+
+}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/model/Vertex.java b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/model/Vertex.java
index 3c24df0..8f93317 100644
--- a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/model/Vertex.java
+++ b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/model/Vertex.java
@@ -2,6 +2,8 @@
 
 import com.terminalvelocitycabbage.engine.client.renderer.elements.VertexAttribute;
 import com.terminalvelocitycabbage.engine.client.renderer.elements.VertexFormat;
+import org.joml.Vector2f;
+import org.joml.Vector3f;
 
 import java.util.Arrays;
 
@@ -26,6 +28,13 @@ public Vertex setXYZPosition(float x, float y, float z) {
         return this;
     }
 
+    /**
+     * @return This vertex with the updated position data
+     */
+    public Vertex setXYZPosition(Vector3f xyzPosition) {
+        return setXYZPosition(xyzPosition.x, xyzPosition.y, xyzPosition.z);
+    }
+
     /**
      * @return This vertex with the updated texture coordinate data
      */
@@ -36,6 +45,13 @@ public Vertex setUV(float u, float v) {
         return this;
     }
 
+    /**
+     * @return This vertex with the updated texture coordinate data
+     */
+    public Vertex setUV(Vector2f uv) {
+        return setUV(uv.x, uv.y);
+    }
+
     /**
      * @return This vertex with the updated normal data
      */
@@ -47,6 +63,13 @@ public Vertex setXYZNormal(float x, float y, float z) {
         return this;
     }
 
+    /**
+     * @return This vertex with the updated normal data
+     */
+    public Vertex setXYZNormal(Vector3f xyzNormal) {
+        return setXYZNormal(xyzNormal.x, xyzNormal.y, xyzNormal.z);
+    }
+
     /**
      * @return This vertex with the updated color data
      */
@@ -70,6 +93,16 @@ public Vertex setRGBAColor(float r, float g, float b, float a) {
         return this;
     }
 
+    /**
+     * @param index The Index of the bone that this vertex should be transformed from
+     * @return This vertex with updated index data
+     */
+    public Vertex setBoneIndex(int index) {
+        var offset = format.getOffset(VertexAttribute.BONE_INDEX);
+        data[offset] = index;
+        return this;
+    }
+
     /**
      * Gets sub-data from the vertex data by component
      * @param element The element for which the data is to be retrieved
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/model/bedrock/BedrockModelData.java b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/model/bedrock/BedrockModelData.java
new file mode 100644
index 0000000..e334aaa
--- /dev/null
+++ b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/model/bedrock/BedrockModelData.java
@@ -0,0 +1,430 @@
+package com.terminalvelocitycabbage.engine.client.renderer.model.bedrock;
+
+import com.electronwill.nightconfig.core.Config;
+import com.electronwill.nightconfig.core.ConfigFormat;
+import com.electronwill.nightconfig.core.io.ConfigParser;
+import com.electronwill.nightconfig.json.JsonFormat;
+import com.github.zafarkhaja.semver.Version;
+import com.terminalvelocitycabbage.engine.client.renderer.elements.VertexAttribute;
+import com.terminalvelocitycabbage.engine.client.renderer.elements.VertexFormat;
+import com.terminalvelocitycabbage.engine.client.renderer.model.Mesh;
+import com.terminalvelocitycabbage.engine.client.renderer.model.Model;
+import com.terminalvelocitycabbage.engine.client.renderer.model.Vertex;
+import com.terminalvelocitycabbage.engine.debug.Log;
+import com.terminalvelocitycabbage.engine.filesystem.resources.Resource;
+import com.terminalvelocitycabbage.engine.util.ConfigUtils;
+import org.joml.Quaternionf;
+import org.joml.Vector2f;
+import org.joml.Vector3f;
+
+import java.util.*;
+
+public class BedrockModelData {
+
+    public static final VertexFormat BEDROCK_VERTEX_FORMAT = VertexFormat
+            .builder()
+            .addElement(VertexAttribute.XYZ_POSITION)
+            .addElement(VertexAttribute.XYZ_NORMAL)
+            .addElement(VertexAttribute.UV)
+            .addElement(VertexAttribute.BONE_INDEX)
+            .build();
+
+    Version formatVersion;
+    BedrockGeometryDescription geometryDescription;
+    BedrockBone[] bones;
+
+    private BedrockModelData(Version formatVersion, BedrockGeometryDescription geometryDescription, BedrockBone[] bones) {
+        this.formatVersion = formatVersion;
+        this.geometryDescription = geometryDescription;
+        this.bones = bones;
+    }
+
+    private static class BedrockCube {
+
+        float[] origin;
+        int[] size;
+        int[] uv;
+        float[] pivot;
+        float[] rotation;
+        float[] scale;
+
+        public BedrockCube(float[] origin, int[] size, int[] uv, float[] pivot, float[] rotation, float[] scale) {
+            this.origin = origin;
+            this.size = size;
+            this.uv = uv;
+            this.pivot = pivot;
+            this.rotation = rotation;
+            this.scale = scale;
+        }
+
+        /**
+         *
+         * Positions are defined by an origin and a size, there will be 3 Vertexes at each position, only difference
+         * Will be the UV.
+         *
+         * Origin is always the smallest of the 3 components, so it will always be at the SWBottom corner, meaning that
+         * the position components of the other 7 vertexes will be either the same as the origin or more North East or Up
+         *
+         * X is E/W (E is +), Y is T/B (T is +), Z is N/S (S is +)
+         *
+         *     "* <-- Origin: (U,V)" the * is the origin, since OpenGL does UV from TL to BR this should already be right
+         *
+         *     Offsets from the origin are defined by the size array 123 + xyz, offsets shown below:
+         *     |   s2   |   s0  |   s2  |   s0   |
+         *     *        1_______2_______3           ---
+         *              |       |BR   BL|
+         *              |   T   |   B   |           s2
+         *              |       |TR   TL|7
+         *      4_______5_______6_______8_______9   ---
+         *      |       |       |       |       |
+         *      |   E   |   N   |   W   |   S   |   s1
+         *      |       |       |       |       |
+         *      10-----11------12------13------14   ---
+         *      For UVs all except Bottom use top/bottom/left/right based on above
+         *      only 13 UV positions matter, numbered above
+         *
+         * The UVs need to be divided by the texture size to get them from 0 to 1
+         *
+         * Naming conventions for the variables in this method:
+         * A vertex will be named based on the cardinal directions plus up/down
+         * nsew for cardinal directions u for up and d for down.
+         *
+         * Vertex net will be the vertex to the North East Top corner of this cuboid
+         *
+         * @return A mesh represented from this cube
+         */
+        public Mesh toMesh(int textureWidth, int textureHeight, int boneIndex) {
+
+            //Initial cube sizes
+            //TODO inflate
+            Vector3f netPos = new Vector3f(size[0], size[1], 0f);
+            Vector3f nebPos = new Vector3f(size[0], 0f, 0f);
+            Vector3f nwtPos = new Vector3f(0f, size[1], 0f);
+            Vector3f nwbPos = new Vector3f(0f, 0f, 0f);
+            Vector3f setPos = new Vector3f(size[0], size[1], size[2]);
+            Vector3f sebPos = new Vector3f(size[0], 0f, size[2]);
+            Vector3f swtPos = new Vector3f(0f, size[1], size[2]);
+            Vector3f swbPos = new Vector3f(0f, 0f, size[2]);
+
+            //Initial cube position
+            netPos.add(origin[0], origin[1], origin[2]);
+            nebPos.add(origin[0], origin[1], origin[2]);
+            nwtPos.add(origin[0], origin[1], origin[2]);
+            nwbPos.add(origin[0], origin[1], origin[2]);
+            setPos.add(origin[0], origin[1], origin[2]);
+            sebPos.add(origin[0], origin[1], origin[2]);
+            swtPos.add(origin[0], origin[1], origin[2]);
+            swbPos.add(origin[0], origin[1], origin[2]);
+
+            //Initial Cube rotation
+            if (rotation.length > 0 && pivot.length > 0) {
+
+                Quaternionf q = new Quaternionf();
+                q.rotateXYZ((float) Math.toRadians(rotation[0]), (float) Math.toRadians(rotation[1]), (float) Math.toRadians(rotation[2]));
+
+                netPos.sub(pivot[0], pivot[1], pivot[2]);
+                nebPos.sub(pivot[0], pivot[1], pivot[2]);
+                nwtPos.sub(pivot[0], pivot[1], pivot[2]);
+                nwbPos.sub(pivot[0], pivot[1], pivot[2]);
+                setPos.sub(pivot[0], pivot[1], pivot[2]);
+                sebPos.sub(pivot[0], pivot[1], pivot[2]);
+                swtPos.sub(pivot[0], pivot[1], pivot[2]);
+                swbPos.sub(pivot[0], pivot[1], pivot[2]);
+
+                netPos.rotate(q);
+                nebPos.rotate(q);
+                nwtPos.rotate(q);
+                nwbPos.rotate(q);
+                setPos.rotate(q);
+                sebPos.rotate(q);
+                swtPos.rotate(q);
+                swbPos.rotate(q);
+
+                netPos.add(pivot[0], pivot[1], pivot[2]);
+                nebPos.add(pivot[0], pivot[1], pivot[2]);
+                nwtPos.add(pivot[0], pivot[1], pivot[2]);
+                nwbPos.add(pivot[0], pivot[1], pivot[2]);
+                setPos.add(pivot[0], pivot[1], pivot[2]);
+                sebPos.add(pivot[0], pivot[1], pivot[2]);
+                swtPos.add(pivot[0], pivot[1], pivot[2]);
+                swbPos.add(pivot[0], pivot[1], pivot[2]);
+            }
+
+            //Face normals
+            Vector3f northNormal = new Vector3f(0, 0, -1);
+            Vector3f eastNormal = new Vector3f(1, 0, 0);
+            Vector3f southNormal = new Vector3f(0, 0, 1);
+            Vector3f westNormal = new Vector3f(-1, 0, 0);
+            Vector3f upNormal = new Vector3f(0, 1, 0);
+            Vector3f downNormal = new Vector3f(0, -1, 0);
+
+            //UVs
+            Vector2f uv1 = new Vector2f(uv[0] + size[2],                                uv[1]).div(textureWidth, textureHeight);
+            Vector2f uv2 = new Vector2f(uv[0] + size[2] + size[0],                      uv[1]).div(textureWidth, textureHeight);
+            Vector2f uv3 = new Vector2f(uv[0] + size[2] + size[0] + size[0],            uv[1] + size[2]).div(textureWidth, textureHeight);
+            Vector2f uv4 = new Vector2f(uv[0],                                             uv[1] + size[2]).div(textureWidth, textureHeight);
+            Vector2f uv5 = new Vector2f(uv[0] + size[2],                                uv[1] + size[2]).div(textureWidth, textureHeight);
+            Vector2f uv6 = new Vector2f(uv[0] + size[2] + size[0],                      uv[1] + size[2]).div(textureWidth, textureHeight);
+            Vector2f uv7 = new Vector2f(uv[0] + size[2] + size[0] + size[0],            uv[1] + size[2]).div(textureWidth, textureHeight);
+            Vector2f uv8 = new Vector2f(uv[0] + size[2] + size[0] + size[2],            uv[1] + size[2]).div(textureWidth, textureHeight);
+            Vector2f uv9 = new Vector2f(uv[0] + size[2] + size[0] + size[2] + size[0],  uv[1] + size[2]).div(textureWidth, textureHeight);
+            Vector2f uv10 = new Vector2f(uv[0],                                            uv[1] + size[2] + size[1]).div(textureWidth, textureHeight);
+            Vector2f uv11 = new Vector2f(uv[0] + size[2],                               uv[1] + size[2] + size[1]).div(textureWidth, textureHeight);
+            Vector2f uv12 = new Vector2f(uv[0] + size[2] + size[0],                     uv[1] + size[2] + size[1]).div(textureWidth, textureHeight);
+            Vector2f uv13 = new Vector2f(uv[0] + size[2] + size[0] + size[2],           uv[1] + size[2] + size[1]).div(textureWidth, textureHeight);
+            Vector2f uv14 = new Vector2f(uv[0] + size[2] + size[0] + size[2] + size[0], uv[1] + size[2] + size[1]).div(textureWidth, textureHeight);
+
+            //North Face
+            Vertex northTL = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(nwtPos).setXYZNormal(northNormal).setUV(uv6).setBoneIndex(boneIndex);
+            Vertex northTR = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(netPos).setXYZNormal(northNormal).setUV(uv5).setBoneIndex(boneIndex);
+            Vertex northBL = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(nwbPos).setXYZNormal(northNormal).setUV(uv12).setBoneIndex(boneIndex);
+            Vertex northBR = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(nebPos).setXYZNormal(northNormal).setUV(uv11).setBoneIndex(boneIndex);
+            Vertex eastTL = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(netPos).setXYZNormal(eastNormal).setUV(uv5).setBoneIndex(boneIndex);
+            Vertex eastTR = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(setPos).setXYZNormal(eastNormal).setUV(uv4).setBoneIndex(boneIndex);
+            Vertex eastBL = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(nebPos).setXYZNormal(eastNormal).setUV(uv11).setBoneIndex(boneIndex);
+            Vertex eastBR = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(sebPos).setXYZNormal(eastNormal).setUV(uv10).setBoneIndex(boneIndex);
+            Vertex southTL = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(setPos).setXYZNormal(southNormal).setUV(uv9).setBoneIndex(boneIndex);
+            Vertex southTR = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(swtPos).setXYZNormal(southNormal).setUV(uv8).setBoneIndex(boneIndex);
+            Vertex southBL = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(sebPos).setXYZNormal(southNormal).setUV(uv14).setBoneIndex(boneIndex);
+            Vertex southBR = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(swbPos).setXYZNormal(southNormal).setUV(uv13).setBoneIndex(boneIndex);
+            Vertex westTL = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(swtPos).setXYZNormal(westNormal).setUV(uv8).setBoneIndex(boneIndex);
+            Vertex westTR = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(nwtPos).setXYZNormal(westNormal).setUV(uv6).setBoneIndex(boneIndex);
+            Vertex westBL = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(swbPos).setXYZNormal(westNormal).setUV(uv13).setBoneIndex(boneIndex);
+            Vertex westBR = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(nwbPos).setXYZNormal(westNormal).setUV(uv12).setBoneIndex(boneIndex);
+            Vertex topTL = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(swtPos).setXYZNormal(upNormal).setUV(uv2).setBoneIndex(boneIndex);
+            Vertex topTR = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(setPos).setXYZNormal(upNormal).setUV(uv1).setBoneIndex(boneIndex);
+            Vertex topBL = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(nwtPos).setXYZNormal(upNormal).setUV(uv6).setBoneIndex(boneIndex);
+            Vertex topBR = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(netPos).setXYZNormal(upNormal).setUV(uv5).setBoneIndex(boneIndex);
+            Vertex bottomTL = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(nebPos).setXYZNormal(downNormal).setUV(uv7).setBoneIndex(boneIndex);
+            Vertex bottomTR = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(nwbPos).setXYZNormal(downNormal).setUV(uv6).setBoneIndex(boneIndex);
+            Vertex bottomBL = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(sebPos).setXYZNormal(downNormal).setUV(uv3).setBoneIndex(boneIndex);
+            Vertex bottomBR = new Vertex(BEDROCK_VERTEX_FORMAT).setXYZPosition(swbPos).setXYZNormal(downNormal).setUV(uv2).setBoneIndex(boneIndex);
+
+            // 0 1 2 3
+            // 4 5 6 7
+            // 8 9 10 11
+            // 12 13 14 15
+            // 16 17 18 19
+            // 20 21 22 23
+
+            Vertex[] vertices = new Vertex[]{
+                    northTL, northTR, northBL, northBR,
+                    eastTL, eastTR, eastBL, eastBR,
+                    southTL, southTR, southBL, southBR,
+                    westTL, westTR, westBL, westBR,
+                    topTL, topTR, topBL, topBR,
+                    bottomTL, bottomTR, bottomBL, bottomBR
+            };
+
+            //All should wind in the TL BL BR, TL, BR, TR order each face
+            //Bottom should be in BR, TR, TL, BR, TL, BL
+            int[] indexes = new int[]{
+                    0, 2, 3, 0, 3, 1,
+                    4, 6, 7, 4, 7, 5,
+                    8, 10, 11, 8, 11, 9,
+                    12, 14, 15, 12, 15, 13,
+                    16, 18, 19, 16, 19, 17,
+                    23, 21, 20, 23, 20, 22,
+            };
+
+            return new Mesh(BEDROCK_VERTEX_FORMAT, vertices, indexes);
+        }
+
+        @Override
+        public String toString() {
+            return "BedrockCube{" +
+                    "origin=" + Arrays.toString(origin) +
+                    ", size=" + Arrays.toString(size) +
+                    ", uv=" + Arrays.toString(uv) +
+                    '}';
+        }
+    }
+
+    private static class BedrockBone {
+
+        String name;
+        String parent;
+        float[] pivot;
+        float[] rotation;
+        float[] scale;
+        BedrockCube[] cubes;
+
+        public BedrockBone(String name, String parent, float[] pivot, float[] rotation, float[] scale, BedrockCube[] cubes) {
+            this.name = name;
+            this.parent = parent;
+            this.pivot = pivot;
+            this.rotation = rotation;
+            this.scale = scale;
+            this.cubes = cubes;
+        }
+
+        @Override
+        public String toString() {
+            return "BedrockBone{" +
+                    "name='" + name + '\'' +
+                    ", parent='" + parent + '\'' +
+                    ", pivot=" + Arrays.toString(pivot) +
+                    ", rotation=" + Arrays.toString(rotation) +
+                    ", scale=" + Arrays.toString(scale) +
+                    ", cubes=" + Arrays.toString(cubes) +
+                    '}';
+        }
+    }
+
+    private static class BedrockGeometryDescription {
+
+        String identifier;
+        int textureWidth;
+        int textureHeight;
+        float visibleBoundsWidth;
+        float visibleBoundsHeight;
+        float[] visibleBoundsOffset;
+
+        public BedrockGeometryDescription(String identifier, int textureWidth, int textureHeight, float visibleBoundsWidth, float visibleBoundsHeight, float[] visibleBoundsOffset) {
+            this.identifier = identifier;
+            this.textureWidth = textureWidth;
+            this.textureHeight = textureHeight;
+            this.visibleBoundsWidth = visibleBoundsWidth;
+            this.visibleBoundsHeight = visibleBoundsHeight;
+            this.visibleBoundsOffset = visibleBoundsOffset;
+        }
+
+        @Override
+        public String toString() {
+            return "BedrockGeometryDescription{" +
+                    "identifier='" + identifier + '\'' +
+                    ", textureWidth=" + textureWidth +
+                    ", textureHeight=" + textureHeight +
+                    ", visibleBoundsWidth=" + visibleBoundsWidth +
+                    ", visibleBoundsHeight=" + visibleBoundsHeight +
+                    ", visibleBoundsOffset=" + Arrays.toString(visibleBoundsOffset) +
+                    '}';
+        }
+    }
+
+    public static class Loader {
+
+        static Version formatVersion;
+        static BedrockGeometryDescription geometryDescription;
+        static BedrockBone[] bones;
+
+        public static BedrockModelData loadModel(Resource modelResource) {
+
+            String resourceString = modelResource.asString();
+
+            ConfigFormat<?> jsonFormat = JsonFormat.fancyInstance();
+            ConfigParser<?> parser = jsonFormat.createParser();
+            Config config = parser.parse(resourceString);
+
+            formatVersion = Version.parse(config.get("format_version"));
+
+            List<Config> subConfig = config.get("minecraft:geometry");
+            Config geometryConfig = subConfig.get(0);
+
+            geometryDescription = parseGeometryDescription(geometryConfig);
+            bones = parseBones(geometryConfig);
+
+            return new BedrockModelData(formatVersion, geometryDescription, bones);
+        }
+
+        private static BedrockBone[] parseBones(Config config) {
+
+            List<Config> boneConfigs = config.get("bones");
+            List<BedrockBone> bones = new ArrayList<>();
+
+            boneConfigs.forEach(bone -> bones.add(parseBone(bone)));
+
+            return bones.toArray(new BedrockBone[0]);
+        }
+
+        private static BedrockBone parseBone(Config config) {
+
+            String name = config.get("name");
+            String parent = config.getOrElse("parent", "none");
+            float[] pivot = ConfigUtils.numberListToFloatArray(config.get("pivot"));
+            float[] rotation = ConfigUtils.numberListToFloatArray(config.get("rotation"));
+            float[] scale = ConfigUtils.numberListToFloatArray(config.get("scale"));
+
+            return new BedrockBone(name, parent, pivot, rotation, scale, parseCubes(config));
+        }
+
+        private static BedrockCube[] parseCubes(Config config) {
+
+            List<Config> cubes = config.get("cubes");
+            List<BedrockCube> cubesList = new ArrayList<>();
+
+            if (cubes != null) cubes.forEach(cube -> cubesList.add(parseCube(cube)));
+
+            return cubesList.toArray(new BedrockCube[0]);
+        }
+
+        private static BedrockCube parseCube(Config cube) {
+
+            float[] origin = ConfigUtils.numberListToFloatArray(cube.get("origin"));
+            int[] size = ConfigUtils.numberListToIntArray(cube.get("size"));
+            int[] uv = ConfigUtils.numberListToIntArray(cube.get("uv"));
+            float[] pivot = ConfigUtils.numberListToFloatArray(cube.get("pivot"));
+            float[] rotation = ConfigUtils.numberListToFloatArray(cube.get("rotation"));
+            //TODO verify inflate
+            float[] scale = ConfigUtils.numberListToFloatArray(cube.get("inflate"));
+
+            return new BedrockCube(origin, size, uv, pivot, rotation, scale);
+        }
+
+        private static BedrockGeometryDescription parseGeometryDescription(Config config) {
+
+            String identifier = config.get("description.identifier");
+            int textureWidth = config.getInt("description.texture_width");
+            int textureHeight = config.getInt("description.texture_height");
+            float visibleBoundsWidth = ((Number) config.get("description.visible_bounds_width")).floatValue();
+            float visibleBoundsHeight = ((Number) config.get("description.visible_bounds_height")).floatValue();
+            float[] visibleBoundsOffset = ConfigUtils.numberListToFloatArray(config.get("description.visible_bounds_offset"));
+
+            return new BedrockGeometryDescription(identifier, textureWidth, textureHeight, visibleBoundsWidth, visibleBoundsHeight, visibleBoundsOffset);
+        }
+
+    }
+
+    public Model toModel() {
+
+        //The information needed to create a model part extracted from all bones
+        List<Mesh> meshesToCompile = new ArrayList<>();
+        Map<String, Model.Bone> modelBones = new HashMap<>();
+
+        //Loop through all bones to get staging data for a model part
+        for (int i = 0; i < bones.length; i++) {
+            //This iteration's bone
+            BedrockBone bone = bones[i];
+
+            //Get all the meshes from these bones and add them to a list for later compilation
+            for (BedrockCube cube : bone.cubes) {
+                //Convert cubes to meshes
+                meshesToCompile.add(cube.toMesh(geometryDescription.textureWidth, geometryDescription.textureHeight, i));
+            }
+
+            //Default Values
+            var partPivot = new Vector3f();
+            var partRotation = new Quaternionf();
+            var partScale = new Vector3f(1);
+            //Fill values if they exist
+            if (bone.pivot.length > 0) partPivot.set(bone.pivot[0], bone.pivot[1], bone.pivot[2]);
+            if (bone.rotation.length > 0) partRotation.rotateXYZ(bone.rotation[0], bone.rotation[1], bone.rotation[2]);
+            if (bone.scale.length > 0) partScale.set(bone.scale[0], bone.scale[1], bone.scale[2]);
+            //Create the part and add it to the list of parts
+            var newPart = new Model.Bone(bone.name, bone.parent, partPivot, partRotation, partScale, i);
+            modelBones.put(newPart.getName(), newPart);
+        }
+
+        //Create a model from all the parts and meshes
+        return new Model(BEDROCK_VERTEX_FORMAT, modelBones, Mesh.of(meshesToCompile));
+    }
+
+    public void print() {
+        Log.info(formatVersion.toString());
+        Log.info(geometryDescription.toString());
+        for (BedrockBone bone : bones) {
+            Log.info(bone.toString());
+        }
+    }
+
+}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/shader/Uniform.java b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/shader/Uniform.java
index 1ddfe39..f813720 100644
--- a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/shader/Uniform.java
+++ b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/shader/Uniform.java
@@ -4,6 +4,9 @@
 import org.joml.Matrix4f;
 import org.lwjgl.system.MemoryStack;
 
+import java.nio.FloatBuffer;
+import java.util.Map;
+
 import static org.lwjgl.opengl.GL20.*;
 
 public class Uniform {
@@ -32,7 +35,7 @@ public void create(int shaderProgram) {
      */
     //TODO add a lot more of these supported uniform types
     public void setUniform(int uniformValue) {
-        glUniform1i(glGetUniformLocation(shaderProgramId, uniformName), uniformValue);
+        glUniform1i(uniformLocation, uniformValue);
     }
 
     /**
@@ -40,7 +43,19 @@ public void setUniform(int uniformValue) {
      */
     public void setUniform(Matrix4f matrix4f) {
         try (MemoryStack stack = MemoryStack.stackPush()) {
-            glUniformMatrix4fv(glGetUniformLocation(shaderProgramId, uniformName), false, matrix4f.get(stack.mallocFloat(16)));
+            glUniformMatrix4fv(uniformLocation, false, matrix4f.get(stack.mallocFloat(16)));
+        }
+    }
+
+    public void setUniforms(Map<Integer, Matrix4f> boneTransformations) {
+        //TODO using memory stack here may result in an overflow of stack space, might need to change to not use this
+        try (MemoryStack stack = MemoryStack.stackPush()) {
+            int length = boneTransformations != null ? boneTransformations.size() : 0;
+            FloatBuffer fb = stack.mallocFloat(16 * length);
+            for (int i = 0; i < length; i++) {
+                boneTransformations.get(i).get(16 * i, fb);
+            }
+            glUniformMatrix4fv(uniformLocation, false, fb);
         }
     }
 
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/window/WindowThread.java b/src/main/java/com/terminalvelocitycabbage/engine/client/window/WindowThread.java
index 16bfe38..9ee75eb 100644
--- a/src/main/java/com/terminalvelocitycabbage/engine/client/window/WindowThread.java
+++ b/src/main/java/com/terminalvelocitycabbage/engine/client/window/WindowThread.java
@@ -39,6 +39,8 @@ public void run() {
         glfwMakeContextCurrent(windowHandle);
         GL.createCapabilities();
 
+        glEnable(GL_DEPTH_TEST);
+
         //Turn on vsync
         //TODO swap this out for a window config apply() && Verify that bgfx may take care of this instead
         glfwSwapInterval(1);
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/graph/Routine.java b/src/main/java/com/terminalvelocitycabbage/engine/graph/Routine.java
index a22a0e1..dd93716 100644
--- a/src/main/java/com/terminalvelocitycabbage/engine/graph/Routine.java
+++ b/src/main/java/com/terminalvelocitycabbage/engine/graph/Routine.java
@@ -11,7 +11,7 @@
 import com.terminalvelocitycabbage.engine.util.EntrypointUtils;
 import com.terminalvelocitycabbage.engine.util.MutableInstant;
 import com.terminalvelocitycabbage.engine.util.Toggle;
-import com.terminalvelocitycabbage.engine.util.touples.Quartet;
+import com.terminalvelocitycabbage.engine.util.tuples.Quartet;
 import com.terminalvelocitycabbage.templates.events.RoutineSystemExecutionEvent;
 
 import java.util.HashMap;
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/mod/ModInfo.java b/src/main/java/com/terminalvelocitycabbage/engine/mod/ModInfo.java
index ffcc71f..460491c 100644
--- a/src/main/java/com/terminalvelocitycabbage/engine/mod/ModInfo.java
+++ b/src/main/java/com/terminalvelocitycabbage/engine/mod/ModInfo.java
@@ -1,7 +1,7 @@
 package com.terminalvelocitycabbage.engine.mod;
 
 import com.github.zafarkhaja.semver.Version;
-import com.terminalvelocitycabbage.engine.util.touples.Pair;
+import com.terminalvelocitycabbage.engine.util.tuples.Pair;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/mod/ModLoader.java b/src/main/java/com/terminalvelocitycabbage/engine/mod/ModLoader.java
index d88cd2e..de9877f 100644
--- a/src/main/java/com/terminalvelocitycabbage/engine/mod/ModLoader.java
+++ b/src/main/java/com/terminalvelocitycabbage/engine/mod/ModLoader.java
@@ -12,7 +12,7 @@
 import com.terminalvelocitycabbage.engine.registry.Registry;
 import com.terminalvelocitycabbage.engine.util.ClassUtils;
 import com.terminalvelocitycabbage.engine.util.Toggle;
-import com.terminalvelocitycabbage.engine.util.touples.Pair;
+import com.terminalvelocitycabbage.engine.util.tuples.Pair;
 
 import javax.management.ReflectionException;
 import java.io.File;
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/registry/RegistryPair.java b/src/main/java/com/terminalvelocitycabbage/engine/registry/RegistryPair.java
index 73d5542..5fefd97 100644
--- a/src/main/java/com/terminalvelocitycabbage/engine/registry/RegistryPair.java
+++ b/src/main/java/com/terminalvelocitycabbage/engine/registry/RegistryPair.java
@@ -1,6 +1,6 @@
 package com.terminalvelocitycabbage.engine.registry;
 
-import com.terminalvelocitycabbage.engine.util.touples.Pair;
+import com.terminalvelocitycabbage.engine.util.tuples.Pair;
 
 /**
  * Represents a result from a registration used for doing things with data as it's being registered
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/ArrayUtils.java b/src/main/java/com/terminalvelocitycabbage/engine/util/ArrayUtils.java
index 92fa79e..9cd09de 100644
--- a/src/main/java/com/terminalvelocitycabbage/engine/util/ArrayUtils.java
+++ b/src/main/java/com/terminalvelocitycabbage/engine/util/ArrayUtils.java
@@ -30,4 +30,31 @@ public static float[] combineFloatArrays(List<float[]> arrays) {
         return compiledData;
     }
 
+    /**
+     * @param list the float list to be converted to an array
+     * @return an array made of the same values as the list
+     */
+    public static float[] floatArrayFromClassList(List<Float> list) {
+        float[] compiledData = new float[list.size()];
+        int currentPosition = 0;
+        for (Float element : list) {
+            compiledData[currentPosition] = element;
+            currentPosition++;
+        }
+        return compiledData;
+    }
+
+    /**
+     * @param list the float list to be converted to an array
+     * @return an array made of the same values as the list
+     */
+    public static int[] intArrayFromClassList(List<Integer> list) {
+        int[] compiledData = new int[list.size()];
+        int currentPosition = 0;
+        for (Integer element : list) {
+            compiledData[currentPosition] = element;
+            currentPosition++;
+        }
+        return compiledData;
+    }
 }
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/ConfigUtils.java b/src/main/java/com/terminalvelocitycabbage/engine/util/ConfigUtils.java
new file mode 100644
index 0000000..a62ff03
--- /dev/null
+++ b/src/main/java/com/terminalvelocitycabbage/engine/util/ConfigUtils.java
@@ -0,0 +1,29 @@
+package com.terminalvelocitycabbage.engine.util;
+
+import org.joml.Vector3f;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ConfigUtils {
+
+    public static Vector3f numberListToVector3f(List<Number> list) {
+        float[] numberArray = numberListToFloatArray(list);
+        return new Vector3f(numberArray[0], numberArray[1], numberArray[2]);
+    }
+
+    public static float[] numberListToFloatArray(List<Number> list) {
+        if (list == null) return new float[0];
+        List<Float> convertedList = new ArrayList<>();
+        list.forEach(n -> convertedList.add(n.floatValue()));
+        return ArrayUtils.floatArrayFromClassList(convertedList);
+    }
+
+    public static int[] numberListToIntArray(List<Number> list) {
+        if (list == null) return new int[0];
+        List<Integer> convertedList = new ArrayList<>();
+        list.forEach(n -> convertedList.add(n.intValue()));
+        return ArrayUtils.intArrayFromClassList(convertedList);
+    }
+
+}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/Easing.java b/src/main/java/com/terminalvelocitycabbage/engine/util/Easing.java
new file mode 100644
index 0000000..6efa4da
--- /dev/null
+++ b/src/main/java/com/terminalvelocitycabbage/engine/util/Easing.java
@@ -0,0 +1,277 @@
+package com.terminalvelocitycabbage.engine.util;
+
+import static java.lang.Math.*;
+import static org.joml.Math.sin;
+
+public class Easing {
+
+    private static final float PI = 3.1415f;
+    private static final float C1 = 1.70158f;
+    private static final float C2 = C1 * 1.525f;
+    private static final float C3 = C1 + 1;
+    private static final float C4 = (2 * PI) / 3f;
+    private static final float C5 = (2 * PI) / 4.5f;
+    private static final float N1 = 7.5625f;
+    private static final float D1 = 2.75f;
+
+    public enum Direction {
+
+        IN("in"),
+        OUT("out"),
+        IN_OUT("in_out");
+
+        private final String name;
+
+        Direction(String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return name;
+        }
+    }
+
+    public enum Function {
+
+        LINEAR("linear"),
+        STEP("step"),
+        SIN("sin"),
+        QUADRATIC("quadratic"),
+        CUBIC("cubic"),
+        QUARTIC("quartic"),
+        QUINTIC("quintic"),
+        EXPONENTIAL("exponential"),
+        CIRCULAR("circular"),
+        BACK("back"),
+        ELASTIC("elsatic"),
+        BOUNCE("bounce");
+
+        private final String name;
+
+        Function(String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return name;
+        }
+    }
+
+    public static float lerp(float start, float end, float progress, Direction direction, Function function) {
+        return (start + (end - start) * ease(direction, function, progress));
+    }
+
+    public static float ease(Direction direction, Function function, float progress) {
+        return switch (direction) {
+            case IN -> easeIn(function, progress);
+            case OUT -> easeOut(function, progress);
+            case IN_OUT -> easeInOut(function, progress);
+        };
+    }
+
+    public static float easeIn(Function function, float progress) {
+        return switch (function) {
+            case LINEAR -> easeInLinear(progress);
+            case STEP -> easeInStep(progress);
+            case SIN -> easeInSin(progress);
+            case QUADRATIC -> easeInQuad(progress);
+            case CUBIC -> easeInCubic(progress);
+            case QUARTIC -> easeInQuart(progress);
+            case QUINTIC -> easeInQuint(progress);
+            case EXPONENTIAL -> easeInExpo(progress);
+            case CIRCULAR -> easeInCirc(progress);
+            case BACK -> easeInBack(progress);
+            case ELASTIC -> easeInElastic(progress);
+            case BOUNCE -> easeInBounce(progress);
+        };
+    }
+
+    public static float easeOut(Function function, float progress) {
+        return switch (function) {
+            case LINEAR -> easeOutLinear(progress);
+            case STEP -> easeOutStep(progress);
+            case SIN -> easeOutSin(progress);
+            case QUADRATIC -> easeOutQuad(progress);
+            case CUBIC -> easeOutCubic(progress);
+            case QUARTIC -> easeOutQuart(progress);
+            case QUINTIC -> easeOutQuint(progress);
+            case EXPONENTIAL -> easeOutExpo(progress);
+            case CIRCULAR -> easeOutCirc(progress);
+            case BACK -> easeOutBack(progress);
+            case ELASTIC -> easeOutElastic(progress);
+            case BOUNCE -> easeOutBounce(progress);
+        };
+    }
+
+    public static float easeInOut(Function function, float progress) {
+        return switch (function) {
+            case LINEAR -> easeInOutLinear(progress);
+            case STEP -> easeInOutStep(progress);
+            case SIN -> easeInOutSin(progress);
+            case QUADRATIC -> easeInOutQuad(progress);
+            case CUBIC -> easeInOutCubic(progress);
+            case QUARTIC -> easeInOutQuart(progress);
+            case QUINTIC -> easeInOutQuint(progress);
+            case EXPONENTIAL -> easeInOutExpo(progress);
+            case CIRCULAR -> easeInOutCirc(progress);
+            case BACK -> easeInOutBack(progress);
+            case ELASTIC -> easeInOutElastic(progress);
+            case BOUNCE -> easeInOutBounce(progress);
+        };
+    }
+
+    public static float easeInLinear(float progress) {
+        return progress;
+    }
+
+    public static float easeOutLinear(float progress) {
+        return progress;
+    }
+
+    public static float easeInOutLinear(float progress) {
+        return progress;
+    }
+
+    public static float easeInStep(float progress) {
+        return progress > 0 ? 1f : 0f;
+    }
+
+    public static float easeOutStep(float progress) {
+        return progress > 0 ? 0f : 1f;
+    }
+
+    public static float easeInOutStep(float progress) {
+        return progress > 0.5 ? 1f : 0f;
+    }
+
+    public static float easeInSin(float progress) {
+        return (float) (1 - cos((progress * PI) / 2f));
+    }
+
+    public static float easeOutSin(float progress) {
+        return sin((progress * PI) / 2f);
+    }
+
+    public static float easeInOutSin(float progress) {
+        return (float) ((-cos(PI * progress) - 1) / 2f);
+    }
+
+    public static float easeInQuad(float progress) {
+        return progress * progress;
+    }
+
+    public static float easeOutQuad(float progress) {
+        return 1 - ((1 - progress) * (1 - progress));
+    }
+
+    public static float easeInOutQuad(float progress) {
+        return progress < 0.5 ? 2 * progress * progress : 1 - (float)pow((-2 * progress) + 2, 2) / 2f;
+    }
+
+    public static float easeInCubic(float progress) {
+        return progress * progress * progress;
+    }
+
+    public static float easeOutCubic(float progress) {
+        return 1 - ((1 - progress) * (1 - progress) * (1 - progress));
+    }
+
+    public static float easeInOutCubic(float progress) {
+        return progress < 0.5 ? 4 * progress * progress * progress : 1 - (float)pow((-2 * progress) + 2, 3) / 2f;
+    }
+
+    public static float easeInQuart(float progress) {
+        return progress * progress * progress * progress;
+    }
+
+    public static float easeOutQuart(float progress) {
+        return 1 - (float)pow(1 - progress, 4);
+    }
+
+    public static float easeInOutQuart(float progress) {
+        return progress < 0.5 ? 8 * progress * progress * progress * progress : 1 - (float)pow(-2 * progress + 2, 4) / 2f;
+    }
+
+    public static float easeInQuint(float progress) {
+        return progress * progress * progress * progress * progress;
+    }
+
+    public static float easeOutQuint(float progress) {
+        return 1 - (float)pow(1 - progress, 5);
+    }
+
+    public static float easeInOutQuint(float progress) {
+        return progress < 0.5 ? 16 * progress * progress * progress * progress * progress : 1 - (float)pow(-2 * progress + 2, 5) / 2f;
+    }
+
+    public static float easeInExpo(float progress) {
+        return progress == 0 ? 0 : (float)pow(2, 10 * progress - 10);
+    }
+
+    public static float easeOutExpo(float progress) {
+        return progress == 1 ? 1 : 1 - (float)pow(2, -10 * progress);
+    }
+
+    public static float easeInOutExpo(float progress) {
+        return progress == 0 ? 0 : progress == 1 ? 1 : progress < 0.5 ? (float)pow(2, 20 * progress - 10) / 2 : (2 - (float)pow(2, -20 * progress + 10)) / 2;
+    }
+
+    public static float easeInCirc(float progress) {
+        return 1 - (float)sqrt(1 - pow(progress, 2));
+    }
+
+    public static float easeOutCirc(float progress) {
+        return (float)sqrt(1 - pow(progress - 1, 2));
+    }
+
+    public static float easeInOutCirc(float progress) {
+        return progress < 0.5 ? (float)(1 - sqrt(1 - pow(2 * progress, 2))) / 2 : (float)(sqrt(1 - pow(-2 * progress + 2, 2)) + 1) / 2;
+    }
+
+    public static float easeInBack(float progress) {
+        return C3 * progress * progress * progress - C1 * progress * progress;
+    }
+
+    public static float easeOutBack(float progress) {
+        return (float)(1 + C3 * pow(progress - 1, 3) + C1 * pow(progress - 1, 2));
+    }
+
+    public static float easeInOutBack(float progress) {
+        return progress < 0.5 ? (float)(pow(2 * progress, 2) * ((C2 + 1) * 2 * progress - C2)) / 2 : (float)(pow(2 * progress - 2, 2) * ((C2 + 1) * (progress * 2 - 2) + C2) + 2) / 2;
+    }
+
+    public static float easeInElastic(float progress) {
+        return progress == 0 ? 0 : progress == 1 ? 1 : (float)-pow(2, 10 * progress - 10) * (float)sin((progress * 10 - 10.75) * C4);
+    }
+
+    public static float easeOutElastic(float progress) {
+        return progress == 0 ? 0 : progress == 1 ? 1 : (float)pow(2, -10 * progress) * (float)sin((progress * 10 - 0.75) * C4) + 1;
+    }
+
+    public static float easeInOutElastic(float progress) {
+        return progress == 0 ? 0 : progress == 1 ? 1 : progress < 0.5
+                ? (float) -(pow(2, 20 * progress - 10) * sin((20 * progress - 11.125) * C5)) / 2
+                : (float) (pow(2, -20 * progress + 10) * sin((20 * progress - 11.125) * C5)) / 2 + 1;
+    }
+
+    public static float easeInBounce(float progress) {
+        return 1 - easeOutBounce(1 - progress);
+    }
+
+    public static float easeOutBounce(float progress) {
+        if (progress < 1 / D1) {
+            return N1 * progress * progress;
+        } else if (progress < 2 / D1) {
+            return N1 * (progress -= 1.5 / D1) * progress + 0.75f;
+        } else if (progress < 2.5 / D1) {
+            return N1 * (progress -= 2.25 / D1) * progress + 0.9375f;
+        } else {
+            return N1 * (progress -= 2.625 / D1) * progress + 0.984375f;
+        }
+    }
+
+    public static float easeInOutBounce(float progress) {
+        return progress < 0.5 ? (1 - easeOutBounce(1 - 2 * progress)) / 2 : (1 + easeOutBounce(2 * progress - 1)) / 2;
+    }
+
+}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Decade.java b/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Decade.java
deleted file mode 100644
index 6e71015..0000000
--- a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Decade.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.terminalvelocitycabbage.engine.util.touples;
-
-public class Decade<A, B, C, D, E, F, G, H, I, J> extends Ennead<A, B, C, D, E, F, G, H, I> {
-
-    private final J value9;
-
-    public Decade(A value0, B value1, C value2, D value3, E value4, F value5, G value6, H value7, I value8, J value9) {
-        super(value0, value1, value2, value3, value4, value5, value6, value7, value8);
-        this.value9 = value9;
-    }
-
-    public J getValue9() {
-        return value9;
-    }
-
-    @Override
-    public String toString() {
-        return "Decade{" +
-                "value0=" + getValue0() +
-                "value1=" + getValue1() +
-                "value2=" + getValue2() +
-                "value3=" + getValue3() +
-                "value4=" + getValue4() +
-                "value5=" + getValue5() +
-                "value6=" + getValue6() +
-                "value7=" + getValue7() +
-                "value8=" + getValue8() +
-                "value9=" + getValue9() +
-                '}';
-    }
-}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Ennead.java b/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Ennead.java
deleted file mode 100644
index b15c518..0000000
--- a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Ennead.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.terminalvelocitycabbage.engine.util.touples;
-
-public class Ennead<A, B, C, D, E, F, G, H, I> extends Octet<A, B, C, D, E, F, G, H> {
-
-    private final I value8;
-
-    public Ennead(A value0, B value1, C value2, D value3, E value4, F value5, G value6, H value7, I value8) {
-        super(value0, value1, value2, value3, value4, value5, value6, value7);
-        this.value8 = value8;
-    }
-
-    public I getValue8() {
-        return value8;
-    }
-
-    @Override
-    public String toString() {
-        return "Ennead{" +
-                "value0=" + getValue0() +
-                "value1=" + getValue1() +
-                "value2=" + getValue2() +
-                "value3=" + getValue3() +
-                "value4=" + getValue4() +
-                "value5=" + getValue5() +
-                "value6=" + getValue6() +
-                "value7=" + getValue7() +
-                "value8=" + getValue8() +
-                '}';
-    }
-}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Octet.java b/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Octet.java
deleted file mode 100644
index b7413d7..0000000
--- a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Octet.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.terminalvelocitycabbage.engine.util.touples;
-
-public class Octet<A, B, C, D, E, F, G, H> extends Septet<A, B, C, D, E, F, G> {
-
-    private final H value7;
-
-    public Octet(A value0, B value1, C value2, D value3, E value4, F value5, G value6, H value7) {
-        super(value0, value1, value2, value3, value4, value5, value6);
-        this.value7 = value7;
-    }
-
-    public H getValue7() {
-        return value7;
-    }
-
-    @Override
-    public String toString() {
-        return "Octet{" +
-                "value0=" + getValue0() +
-                "value1=" + getValue1() +
-                "value2=" + getValue2() +
-                "value3=" + getValue3() +
-                "value4=" + getValue4() +
-                "value5=" + getValue5() +
-                "value6=" + getValue6() +
-                "value7=" + getValue7() +
-                '}';
-    }
-}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Quintet.java b/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Quintet.java
deleted file mode 100644
index e6bb75a..0000000
--- a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Quintet.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.terminalvelocitycabbage.engine.util.touples;
-
-public class Quintet<A, B, C, D, E> extends Quartet<A, B, C, D> {
-
-    private final E value4;
-
-    public Quintet(A value0, B value1, C value2, D value3, E value4) {
-        super(value0, value1, value2, value3);
-        this.value4 = value4;
-    }
-
-    public E getValue4() {
-        return value4;
-    }
-
-    @Override
-    public String toString() {
-        return "Quintet{" +
-                "value0=" + getValue0() +
-                "value1=" + getValue1() +
-                "value2=" + getValue2() +
-                "value3=" + getValue3() +
-                "value4=" + getValue4() +
-                '}';
-    }
-}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Septet.java b/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Septet.java
deleted file mode 100644
index 1b57801..0000000
--- a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Septet.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.terminalvelocitycabbage.engine.util.touples;
-
-public class Septet<A, B, C, D, E, F, G> extends Sextet<A, B, C, D, E, F> {
-
-    private final G value6;
-
-    public Septet(A value0, B value1, C value2, D value3, E value4, F value5, G value6) {
-        super(value0, value1, value2, value3, value4, value5);
-        this.value6 = value6;
-    }
-
-    public G getValue6() {
-        return value6;
-    }
-
-    @Override
-    public String toString() {
-        return "Septet{" +
-                "value0=" + getValue0() +
-                "value1=" + getValue1() +
-                "value2=" + getValue2() +
-                "value3=" + getValue3() +
-                "value4=" + getValue4() +
-                "value5=" + getValue5() +
-                "value6=" + getValue6() +
-                '}';
-    }
-}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Sextet.java b/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Sextet.java
deleted file mode 100644
index e77217a..0000000
--- a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Sextet.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.terminalvelocitycabbage.engine.util.touples;
-
-public class Sextet<A, B, C, D, E, F> extends Quintet<A, B, C, D, E> {
-
-    private final F value5;
-
-    public Sextet(A value0, B value1, C value2, D value3, E value4, F value5) {
-        super(value0, value1, value2, value3, value4);
-        this.value5 = value5;
-    }
-
-    public F getValue5() {
-        return value5;
-    }
-
-    @Override
-    public String toString() {
-        return "Sextet{" +
-                "value0=" + getValue0() +
-                "value1=" + getValue1() +
-                "value2=" + getValue2() +
-                "value3=" + getValue3() +
-                "value4=" + getValue4() +
-                "value5=" + getValue5() +
-                '}';
-    }
-}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Decade.java b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Decade.java
new file mode 100644
index 0000000..8083771
--- /dev/null
+++ b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Decade.java
@@ -0,0 +1,67 @@
+package com.terminalvelocitycabbage.engine.util.tuples;
+
+public class Decade<A, B, C, D, E, F, G, H, I, J> extends Ennead<A, B, C, D, E, F, G, H, I> {
+
+    private final J value9;
+
+    public Decade(A value0, B value1, C value2, D value3, E value4, F value5, G value6, H value7, I value8, J value9) {
+        super(value0, value1, value2, value3, value4, value5, value6, value7, value8);
+        this.value9 = value9;
+    }
+
+    public Decade(Unit<A> tuple, B value1, C value2, D value3, E value4, F value5, G value6, H value7, I value8, J value9) {
+        this(tuple.getValue0(), value1, value2, value3, value4, value5, value6, value7, value8, value9);
+    }
+
+    public Decade(Pair<A, B> tuple, C value2, D value3, E value4, F value5, G value6, H value7, I value8, J value9) {
+        this(tuple.getValue0(), tuple.getValue1(), value2, value3, value4, value5, value6, value7, value8, value9);
+    }
+
+    public Decade(Triplet<A, B, C> tuple, D value3, E value4, F value5, G value6, H value7, I value8, J value9) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), value3, value4, value5, value6, value7, value8, value9);
+    }
+
+    public Decade(Quartet<A, B, C, D> tuple, E value4, F value5, G value6, H value7, I value8, J value9) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), value4, value5, value6, value7, value8, value9);
+    }
+
+    public Decade(Quintet<A, B, C, D, E> tuple, F value5, G value6, H value7, I value8, J value9) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), tuple.getValue4(), value5, value6, value7, value8, value9);
+    }
+
+    public Decade(Sextet<A, B, C, D, E, F> tuple, G value6, H value7, I value8, J value9) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), tuple.getValue4(), tuple.getValue5(), value6, value7, value8, value9);
+    }
+
+    public Decade(Septet<A, B, C, D, E, F, G> tuple, H value7, I value8, J value9) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), tuple.getValue4(), tuple.getValue5(), tuple.getValue6(), value7, value8, value9);
+    }
+
+    public Decade(Octet<A, B, C, D, E, F, G, H> tuple, I value8, J value9) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), tuple.getValue4(), tuple.getValue5(), tuple.getValue6(), tuple.getValue7(), value8, value9);
+    }
+
+    public Decade(Ennead<A, B, C, D, E, F, G, H, I> tuple, J value9) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), tuple.getValue4(), tuple.getValue5(), tuple.getValue6(), tuple.getValue7(), tuple.getValue8(), value9);
+    }
+
+    public J getValue9() {
+        return value9;
+    }
+
+    @Override
+    public String toString() {
+        return "Decade{" +
+                "value0=" + getValue0() +
+                "value1=" + getValue1() +
+                "value2=" + getValue2() +
+                "value3=" + getValue3() +
+                "value4=" + getValue4() +
+                "value5=" + getValue5() +
+                "value6=" + getValue6() +
+                "value7=" + getValue7() +
+                "value8=" + getValue8() +
+                "value9=" + getValue9() +
+                '}';
+    }
+}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Ennead.java b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Ennead.java
new file mode 100644
index 0000000..8d71e59
--- /dev/null
+++ b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Ennead.java
@@ -0,0 +1,62 @@
+package com.terminalvelocitycabbage.engine.util.tuples;
+
+public class Ennead<A, B, C, D, E, F, G, H, I> extends Octet<A, B, C, D, E, F, G, H> {
+
+    private final I value8;
+
+    public Ennead(A value0, B value1, C value2, D value3, E value4, F value5, G value6, H value7, I value8) {
+        super(value0, value1, value2, value3, value4, value5, value6, value7);
+        this.value8 = value8;
+    }
+
+    public Ennead(Unit<A> tuple, B value1, C value2, D value3, E value4, F value5, G value6, H value7, I value8) {
+        this(tuple.getValue0(), value1, value2, value3, value4, value5, value6, value7, value8);
+    }
+
+    public Ennead(Pair<A, B> tuple, C value2, D value3, E value4, F value5, G value6, H value7, I value8) {
+        this(tuple.getValue0(), tuple.getValue1(), value2, value3, value4, value5, value6, value7, value8);
+    }
+
+    public Ennead(Triplet<A, B, C> tuple, D value3, E value4, F value5, G value6, H value7, I value8) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), value3, value4, value5, value6, value7, value8);
+    }
+
+    public Ennead(Quartet<A, B, C, D> tuple, E value4, F value5, G value6, H value7, I value8) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), value4, value5, value6, value7, value8);
+    }
+
+    public Ennead(Quintet<A, B, C, D, E> tuple, F value5, G value6, H value7, I value8) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), tuple.getValue4(), value5, value6, value7, value8);
+    }
+
+    public Ennead(Sextet<A, B, C, D, E, F> tuple, G value6, H value7, I value8) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), tuple.getValue4(), tuple.getValue5(), value6, value7, value8);
+    }
+
+    public Ennead(Septet<A, B, C, D, E, F, G> tuple, H value7, I value8) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), tuple.getValue4(), tuple.getValue5(), tuple.getValue6(), value7, value8);
+    }
+
+    public Ennead(Octet<A, B, C, D, E, F, G, H> tuple, I value8) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), tuple.getValue4(), tuple.getValue5(), tuple.getValue6(), tuple.getValue7(), value8);
+    }
+
+    public I getValue8() {
+        return value8;
+    }
+
+    @Override
+    public String toString() {
+        return "Ennead{" +
+                "value0=" + getValue0() +
+                "value1=" + getValue1() +
+                "value2=" + getValue2() +
+                "value3=" + getValue3() +
+                "value4=" + getValue4() +
+                "value5=" + getValue5() +
+                "value6=" + getValue6() +
+                "value7=" + getValue7() +
+                "value8=" + getValue8() +
+                '}';
+    }
+}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Octet.java b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Octet.java
new file mode 100644
index 0000000..1ba97f9
--- /dev/null
+++ b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Octet.java
@@ -0,0 +1,57 @@
+package com.terminalvelocitycabbage.engine.util.tuples;
+
+public class Octet<A, B, C, D, E, F, G, H> extends Septet<A, B, C, D, E, F, G> {
+
+    private final H value7;
+
+    public Octet(A value0, B value1, C value2, D value3, E value4, F value5, G value6, H value7) {
+        super(value0, value1, value2, value3, value4, value5, value6);
+        this.value7 = value7;
+    }
+
+    public Octet(Unit<A> tuple, B value1, C value2, D value3, E value4, F value5, G value6, H value7) {
+        this(tuple.getValue0(), value1, value2, value3, value4, value5, value6, value7);
+    }
+
+    public Octet(Pair<A, B> tuple, C value2, D value3, E value4, F value5, G value6, H value7) {
+        this(tuple.getValue0(), tuple.getValue1(), value2, value3, value4, value5, value6, value7);
+    }
+
+    public Octet(Triplet<A, B, C> tuple, D value3, E value4, F value5, G value6, H value7) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), value3, value4, value5, value6, value7);
+    }
+
+    public Octet(Quartet<A, B, C, D> tuple, E value4, F value5, G value6, H value7) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), value4, value5, value6, value7);
+    }
+
+    public Octet(Quintet<A, B, C, D, E> tuple, F value5, G value6, H value7) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), tuple.getValue4(), value5, value6, value7);
+    }
+
+    public Octet(Sextet<A, B, C, D, E, F> tuple, G value6, H value7) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), tuple.getValue4(), tuple.getValue5(), value6, value7);
+    }
+
+    public Octet(Septet<A, B, C, D, E, F, G> tuple, H value7) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), tuple.getValue4(), tuple.getValue5(), tuple.getValue6(), value7);
+    }
+
+    public H getValue7() {
+        return value7;
+    }
+
+    @Override
+    public String toString() {
+        return "Octet{" +
+                "value0=" + getValue0() +
+                "value1=" + getValue1() +
+                "value2=" + getValue2() +
+                "value3=" + getValue3() +
+                "value4=" + getValue4() +
+                "value5=" + getValue5() +
+                "value6=" + getValue6() +
+                "value7=" + getValue7() +
+                '}';
+    }
+}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Pair.java b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Pair.java
similarity index 73%
rename from src/main/java/com/terminalvelocitycabbage/engine/util/touples/Pair.java
rename to src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Pair.java
index 7ddbedb..10d088b 100644
--- a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Pair.java
+++ b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Pair.java
@@ -1,4 +1,4 @@
-package com.terminalvelocitycabbage.engine.util.touples;
+package com.terminalvelocitycabbage.engine.util.tuples;
 
 public class Pair<A, B> extends Unit<A> {
 
@@ -9,6 +9,10 @@ public Pair(A value0, B value1) {
         this.value1 = value1;
     }
 
+    public Pair(Unit<A> tuple, B value1) {
+        this(tuple.getValue0(), value1);
+    }
+
     public B getValue1() {
         return value1;
     }
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Quartet.java b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Quartet.java
similarity index 54%
rename from src/main/java/com/terminalvelocitycabbage/engine/util/touples/Quartet.java
rename to src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Quartet.java
index de03f84..a83170b 100644
--- a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Quartet.java
+++ b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Quartet.java
@@ -1,4 +1,4 @@
-package com.terminalvelocitycabbage.engine.util.touples;
+package com.terminalvelocitycabbage.engine.util.tuples;
 
 public class Quartet<A, B, C, D> extends Triplet<A, B, C> {
 
@@ -9,6 +9,18 @@ public Quartet(A value0, B value1, C value2, D value3) {
         this.value3 = value3;
     }
 
+    public Quartet(Unit<A> tuple, B value1, C value2, D value3) {
+        this(tuple.getValue0(), value1, value2, value3);
+    }
+
+    public Quartet(Pair<A, B> tuple, C value2, D value3) {
+        this(tuple.getValue0(), tuple.getValue1(), value2, value3);
+    }
+
+    public Quartet(Triplet<A, B, C> tuple, D value3) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), value3);
+    }
+
     public D getValue3() {
         return value3;
     }
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Quintet.java b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Quintet.java
new file mode 100644
index 0000000..e4749ed
--- /dev/null
+++ b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Quintet.java
@@ -0,0 +1,42 @@
+package com.terminalvelocitycabbage.engine.util.tuples;
+
+public class Quintet<A, B, C, D, E> extends Quartet<A, B, C, D> {
+
+    private final E value4;
+
+    public Quintet(A value0, B value1, C value2, D value3, E value4) {
+        super(value0, value1, value2, value3);
+        this.value4 = value4;
+    }
+
+    public Quintet(Unit<A> tuple, B value1, C value2, D value3, E value4) {
+        this(tuple.getValue0(), value1, value2, value3, value4);
+    }
+
+    public Quintet(Pair<A, B> tuple, C value2, D value3, E value4) {
+        this(tuple.getValue0(), tuple.getValue1(), value2, value3, value4);
+    }
+
+    public Quintet(Triplet<A, B, C> tuple, D value3, E value4) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), value3, value4);
+    }
+
+    public Quintet(Quartet<A, B, C, D> tuple, E value4) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), value4);
+    }
+
+    public E getValue4() {
+        return value4;
+    }
+
+    @Override
+    public String toString() {
+        return "Quintet{" +
+                "value0=" + getValue0() +
+                "value1=" + getValue1() +
+                "value2=" + getValue2() +
+                "value3=" + getValue3() +
+                "value4=" + getValue4() +
+                '}';
+    }
+}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Septet.java b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Septet.java
new file mode 100644
index 0000000..e94b434
--- /dev/null
+++ b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Septet.java
@@ -0,0 +1,52 @@
+package com.terminalvelocitycabbage.engine.util.tuples;
+
+public class Septet<A, B, C, D, E, F, G> extends Sextet<A, B, C, D, E, F> {
+
+    private final G value6;
+
+    public Septet(A value0, B value1, C value2, D value3, E value4, F value5, G value6) {
+        super(value0, value1, value2, value3, value4, value5);
+        this.value6 = value6;
+    }
+
+    public Septet(Unit<A> tuple, B value1, C value2, D value3, E value4, F value5, G value6) {
+        this(tuple.getValue0(), value1, value2, value3, value4, value5, value6);
+    }
+
+    public Septet(Pair<A, B> tuple, C value2, D value3, E value4, F value5, G value6) {
+        this(tuple.getValue0(), tuple.getValue1(), value2, value3, value4, value5, value6);
+    }
+
+    public Septet(Triplet<A, B, C> tuple, D value3, E value4, F value5, G value6) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), value3, value4, value5, value6);
+    }
+
+    public Septet(Quartet<A, B, C, D> tuple, E value4, F value5, G value6) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), value4, value5, value6);
+    }
+
+    public Septet(Quintet<A, B, C, D, E> tuple, F value5, G value6) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), tuple.getValue4(), value5, value6);
+    }
+
+    public Septet(Sextet<A, B, C, D, E, F> tuple, G value6) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), tuple.getValue4(), tuple.getValue5(), value6);
+    }
+
+    public G getValue6() {
+        return value6;
+    }
+
+    @Override
+    public String toString() {
+        return "Septet{" +
+                "value0=" + getValue0() +
+                "value1=" + getValue1() +
+                "value2=" + getValue2() +
+                "value3=" + getValue3() +
+                "value4=" + getValue4() +
+                "value5=" + getValue5() +
+                "value6=" + getValue6() +
+                '}';
+    }
+}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Sextet.java b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Sextet.java
new file mode 100644
index 0000000..7a55e8d
--- /dev/null
+++ b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Sextet.java
@@ -0,0 +1,47 @@
+package com.terminalvelocitycabbage.engine.util.tuples;
+
+public class Sextet<A, B, C, D, E, F> extends Quintet<A, B, C, D, E> {
+
+    private final F value5;
+
+    public Sextet(A value0, B value1, C value2, D value3, E value4, F value5) {
+        super(value0, value1, value2, value3, value4);
+        this.value5 = value5;
+    }
+
+    public Sextet(Unit<A> tuple, B value1, C value2, D value3, E value4, F value5) {
+        this(tuple.getValue0(), value1, value2, value3, value4, value5);
+    }
+
+    public Sextet(Pair<A, B> tuple, C value2, D value3, E value4, F value5) {
+        this(tuple.getValue0(), tuple.getValue1(), value2, value3, value4, value5);
+    }
+
+    public Sextet(Triplet<A, B, C> tuple, D value3, E value4, F value5) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), value3, value4, value5);
+    }
+
+    public Sextet(Quartet<A, B, C, D> tuple, E value4, F value5) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), value4, value5);
+    }
+
+    public Sextet(Quintet<A, B, C, D, E> tuple, F value5) {
+        this(tuple.getValue0(), tuple.getValue1(), tuple.getValue2(), tuple.getValue3(), tuple.getValue4(), value5);
+    }
+
+    public F getValue5() {
+        return value5;
+    }
+
+    @Override
+    public String toString() {
+        return "Sextet{" +
+                "value0=" + getValue0() +
+                "value1=" + getValue1() +
+                "value2=" + getValue2() +
+                "value3=" + getValue3() +
+                "value4=" + getValue4() +
+                "value5=" + getValue5() +
+                '}';
+    }
+}
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Triplet.java b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Triplet.java
similarity index 63%
rename from src/main/java/com/terminalvelocitycabbage/engine/util/touples/Triplet.java
rename to src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Triplet.java
index b2fab96..c13b2ff 100644
--- a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Triplet.java
+++ b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Triplet.java
@@ -1,4 +1,4 @@
-package com.terminalvelocitycabbage.engine.util.touples;
+package com.terminalvelocitycabbage.engine.util.tuples;
 
 public class Triplet<A, B, C> extends Pair<A, B> {
 
@@ -9,6 +9,14 @@ public Triplet(A value0, B value1, C value2) {
         this.value2 = value2;
     }
 
+    public Triplet(Unit<A> tuple, B value1, C value2) {
+        this(tuple.getValue0(), value1, value2);
+    }
+
+    public Triplet(Pair<A, B> tuple, C value2) {
+        this(tuple.getValue0(), tuple.getValue1(), value2);
+    }
+
     public C getValue2() {
         return value2;
     }
diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Unit.java b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Unit.java
similarity index 84%
rename from src/main/java/com/terminalvelocitycabbage/engine/util/touples/Unit.java
rename to src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Unit.java
index 5f87f95..ef28eed 100644
--- a/src/main/java/com/terminalvelocitycabbage/engine/util/touples/Unit.java
+++ b/src/main/java/com/terminalvelocitycabbage/engine/util/tuples/Unit.java
@@ -1,4 +1,4 @@
-package com.terminalvelocitycabbage.engine.util.touples;
+package com.terminalvelocitycabbage.engine.util.tuples;
 
 public class Unit<A> {
 
diff --git a/src/main/java/com/terminalvelocitycabbage/templates/ecs/components/ModelAnimationControllerComponent.java b/src/main/java/com/terminalvelocitycabbage/templates/ecs/components/ModelAnimationControllerComponent.java
new file mode 100644
index 0000000..dd86dd8
--- /dev/null
+++ b/src/main/java/com/terminalvelocitycabbage/templates/ecs/components/ModelAnimationControllerComponent.java
@@ -0,0 +1,22 @@
+package com.terminalvelocitycabbage.templates.ecs.components;
+
+import com.terminalvelocitycabbage.engine.client.renderer.animation.AnimationController;
+import com.terminalvelocitycabbage.engine.ecs.Component;
+
+public class ModelAnimationControllerComponent implements Component {
+
+    AnimationController animationController;
+
+    @Override
+    public void setDefaults() {
+        animationController = null;
+    }
+
+    public AnimationController getAnimationController() {
+        return animationController;
+    }
+
+    public void setAnimationController(AnimationController animationController) {
+        this.animationController = animationController;
+    }
+}
diff --git a/src/main/java/com/terminalvelocitycabbage/templates/ecs/components/ModelComponent.java b/src/main/java/com/terminalvelocitycabbage/templates/ecs/components/ModelComponent.java
new file mode 100644
index 0000000..c2fc683
--- /dev/null
+++ b/src/main/java/com/terminalvelocitycabbage/templates/ecs/components/ModelComponent.java
@@ -0,0 +1,27 @@
+package com.terminalvelocitycabbage.templates.ecs.components;
+
+import com.terminalvelocitycabbage.engine.client.renderer.model.Model;
+import com.terminalvelocitycabbage.engine.ecs.Component;
+
+public class ModelComponent implements Component {
+
+    Model model;
+
+    @Override
+    public void setDefaults() {
+        model = null;
+    }
+
+    @Override
+    public void cleanup() {
+        model.cleanup();
+    }
+
+    public Model getModel() {
+        return model;
+    }
+
+    public void setModel(Model model) {
+        this.model = model;
+    }
+}
diff --git a/src/main/java/com/terminalvelocitycabbage/templates/ecs/components/TransformationComponent.java b/src/main/java/com/terminalvelocitycabbage/templates/ecs/components/TransformationComponent.java
index e794eed..030706f 100644
--- a/src/main/java/com/terminalvelocitycabbage/templates/ecs/components/TransformationComponent.java
+++ b/src/main/java/com/terminalvelocitycabbage/templates/ecs/components/TransformationComponent.java
@@ -27,6 +27,10 @@ public Vector3f getPosition() {
         return position;
     }
 
+    public TransformationComponent setPosition(Vector3f position) {
+        return setPosition(position.x, position.y, position.z);
+    }
+
     public TransformationComponent setPosition(float x, float y, float z) {
         this.position.set(x, y, z);
         dirty = true;
@@ -38,7 +42,7 @@ public Quaternionf getRotation() {
     }
 
     public TransformationComponent rotate(float x, float y, float z) {
-        this.rotation.rotateYXZ((float) Math.toRadians(x), (float) Math.toRadians(y), (float) Math.toRadians(z));
+        this.rotation.rotateXYZ((float) Math.toRadians(x), (float) Math.toRadians(y), (float) Math.toRadians(z));
         dirty = true;
         return this;
     }