diff --git a/.gitignore b/.gitignore
index aace89d..62c8935 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,55 +1 @@
-.gradle
-gradle/
-build/
-
-
-### IntelliJ IDEA ###
-/.idea/workspace.xml
-/.idea/usage.statistics.xml
-/.idea/uiDesigner.xml
-*.iws
-*.iml
-*.ipr
-out/
-!**/src/main/**/out/
-!**/src/test/**/out/
-
-### Eclipse ###
-.apt_generated
-.classpath
-.factorypath
-.project
-.settings
-.springBeans
-.sts4-cache
-bin/
-!**/src/main/**/bin/
-!**/src/test/**/bin/
-
-### NetBeans ###
-/nbproject/private/
-/nbbuild/
-/dist/
-/nbdist/
-/.nb-gradle/
-
-### VS Code ###
-.vscode/
-
-### OTHER ###
-/wiki
-.DS_Store
-*.bat
-*.conf
-*.dll
-*.dylib
-*.jar
-*.jpg
-*.mhr
-*.ogg
-*.sh
-*.so
-*.ttf
-*.wav
-*.zip
-touch.txt
+.idea/
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index 6030a3d..0000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index 7bcf8e5..0000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
deleted file mode 100644
index fdc392f..0000000
--- a/.idea/jarRepositories.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index a4d2007..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 35eb1dd..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.gitattributes b/Engine/.gitattributes
similarity index 100%
rename from .gitattributes
rename to Engine/.gitattributes
diff --git a/Engine/.gitignore b/Engine/.gitignore
new file mode 100644
index 0000000..331c8ba
--- /dev/null
+++ b/Engine/.gitignore
@@ -0,0 +1,7 @@
+.gradle/
+gradle/
+gradlew
+gradlew.bat
+build/
+
+.idea/
\ No newline at end of file
diff --git a/Engine/build.gradle.kts b/Engine/build.gradle.kts
new file mode 100644
index 0000000..8a80347
--- /dev/null
+++ b/Engine/build.gradle.kts
@@ -0,0 +1,53 @@
+plugins {
+ java
+ id("com.gradleup.shadow") version "8.3.0"
+}
+
+subprojects {
+ apply(plugin = "java")
+ apply(plugin = "com.gradleup.shadow")
+ java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(22)
+ }
+ withSourcesJar()
+ withJavadocJar()
+ }
+}
+
+val lwjglVersion = "3.3.4"
+val jomlVersion = "1.10.7"
+val jomlPrimitivesVersion = "1.10.0"
+
+val lwjglNatives = Pair(
+ System.getProperty("os.name")!!,
+ System.getProperty("os.arch")!!
+).let { (name, arch) ->
+ when {
+ arrayOf("Linux", "SunOS", "Unit").any { name.startsWith(it) } ->
+ if (arrayOf("arm", "aarch64").any { arch.startsWith(it) })
+ "natives-linux${if (arch.contains("64") || arch.startsWith("armv8")) "-arm64" else "-arm32"}"
+ else if (arch.startsWith("ppc"))
+ "natives-linux-ppc64le"
+ else if (arch.startsWith("riscv"))
+ "natives-linux-riscv64"
+ else
+ "natives-linux"
+
+ arrayOf("Mac OS X", "Darwin").any { name.startsWith(it) } ->
+ "natives-macos${if (arch.startsWith("aarch64")) "-arm64" else ""}"
+
+ arrayOf("Windows").any { name.startsWith(it) } ->
+ if (arch.contains("64"))
+ "natives-windows${if (arch.startsWith("aarch64")) "-arm64" else ""}"
+ else
+ "natives-windows-x86"
+
+ else ->
+ throw Error("Unrecognized or unsupported platform. Please set \"lwjglNatives\" manually")
+ }
+}
+extra["lwjglVersion"] = lwjglVersion
+extra["jomlVersion"] = jomlVersion
+extra["jomlPrimitivesVersion"] = jomlPrimitivesVersion
+extra["lwjglNatives"] = lwjglNatives
\ No newline at end of file
diff --git a/Engine/settings.gradle.kts b/Engine/settings.gradle.kts
new file mode 100644
index 0000000..7d8723a
--- /dev/null
+++ b/Engine/settings.gradle.kts
@@ -0,0 +1,7 @@
+rootProject.name = "Engine"
+include(":core")
+project(":core").projectDir = file("src/core")
+include(":opengl")
+project(":opengl").projectDir = file("src/opengl")
+include(":rendering")
+project(":rendering").projectDir = file("src/rendering")
\ No newline at end of file
diff --git a/Engine/src/core/build.gradle.kts b/Engine/src/core/build.gradle.kts
new file mode 100644
index 0000000..94ea372
--- /dev/null
+++ b/Engine/src/core/build.gradle.kts
@@ -0,0 +1,59 @@
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+
+repositories {
+ mavenCentral()
+}
+
+plugins {
+ java
+ id("com.gradleup.shadow") version "8.3.0"
+}
+
+val lwjglVersion: String by rootProject.extra
+val jomlVersion: String by rootProject.extra
+val jomlPrimitivesVersion: String by rootProject.extra
+val lwjglNatives: String by rootProject.extra
+
+sourceSets {
+ val main by getting {
+ java.srcDir("java")
+ }
+}
+
+dependencies {
+ annotationProcessor("org.projectlombok:lombok:1.18.34")
+ implementation("org.projectlombok:lombok:1.18.34")
+ annotationProcessor("javax.annotation:javax.annotation-api:1.3.2")
+ implementation("javax.annotation:javax.annotation-api:1.3.2")
+
+ implementation(platform("org.lwjgl:lwjgl-bom:$lwjglVersion"))
+ implementation("org.lwjgl", "lwjgl")
+ runtimeOnly("org.lwjgl", "lwjgl", classifier = lwjglNatives)
+
+ implementation("org.lwjgl", "lwjgl-glfw")
+ runtimeOnly("org.lwjgl", "lwjgl-glfw", classifier = lwjglNatives)
+
+ implementation("org.lwjgl", "lwjgl-stb")
+ runtimeOnly("org.lwjgl", "lwjgl-stb", classifier = lwjglNatives)
+
+ implementation("org.joml", "joml", jomlVersion)
+ implementation("org.joml", "joml-primitives", jomlPrimitivesVersion)
+
+ shadow(localGroovy())
+ shadow(gradleApi())
+
+ //implementation("org.lwjgl", "lwjgl-nuklear")
+ //implementation("org.lwjgl", "lwjgl-openal")
+ //implementation("org.lwjgl", "lwjgl-remotery")
+ //runtimeOnly("org.lwjgl", "lwjgl-nuklear", classifier = lwjglNatives)
+ //runtimeOnly("org.lwjgl", "lwjgl-openal", classifier = lwjglNatives)
+ //runtimeOnly("org.lwjgl", "lwjgl-remotery", classifier = lwjglNatives)
+}
+
+tasks.named("shadowJar") {
+ archiveFileName.set("dev.euph.engine.core.jar")
+}
+
+configurations {
+ shadow
+}
\ No newline at end of file
diff --git a/Engine/src/core/java/dev/euph/engine/core/data/Archetype.java b/Engine/src/core/java/dev/euph/engine/core/data/Archetype.java
new file mode 100644
index 0000000..e3b841d
--- /dev/null
+++ b/Engine/src/core/java/dev/euph/engine/core/data/Archetype.java
@@ -0,0 +1,37 @@
+package dev.euph.engine.core.data;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@Getter
+@EqualsAndHashCode
+public class Archetype {
+ private final Set> componentClasses;
+
+ @SafeVarargs
+ public Archetype(Class extends T>... componentClasses) {
+ this.componentClasses = new HashSet<>(Arrays.asList(componentClasses));
+ }
+
+ public Archetype(Set> componentClasses) {
+ this.componentClasses = new HashSet<>(componentClasses);
+ }
+
+ public Archetype(List> componentClasses) {
+ this.componentClasses = new HashSet<>(componentClasses);
+ }
+
+ public boolean contains(Class extends T> componentClass) {
+ for (Class extends T> _componentClass : componentClasses) {
+ if (componentClass.isAssignableFrom(_componentClass)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/Engine/src/core/java/dev/euph/engine/core/data/ImageLoader.java b/Engine/src/core/java/dev/euph/engine/core/data/ImageLoader.java
new file mode 100644
index 0000000..f463f75
--- /dev/null
+++ b/Engine/src/core/java/dev/euph/engine/core/data/ImageLoader.java
@@ -0,0 +1,27 @@
+package dev.euph.engine.core.data;
+
+import org.lwjgl.BufferUtils;
+
+import java.nio.ByteBuffer;
+import java.nio.IntBuffer;
+
+import static org.lwjgl.stb.STBImage.*;
+
+
+public class ImageLoader {
+ public record Image(ByteBuffer data, int width, int height) {
+ }
+
+ public static Image loadImage(String path) {
+ IntBuffer width = BufferUtils.createIntBuffer(1);
+ IntBuffer height = BufferUtils.createIntBuffer(1);
+ IntBuffer channels = BufferUtils.createIntBuffer(1);
+ ByteBuffer imageBuffer = stbi_load(path, width, height, channels, 0);
+ if(imageBuffer == null) {
+ throw new RuntimeException("Failed to load image: " + stbi_failure_reason());
+ }
+ Image image = new Image(imageBuffer, width.get(0), height.get(0));
+ stbi_image_free(imageBuffer);
+ return image;
+ }
+}
diff --git a/Engine/src/core/java/dev/euph/engine/core/data/octree/Octree.java b/Engine/src/core/java/dev/euph/engine/core/data/octree/Octree.java
new file mode 100644
index 0000000..062ca6a
--- /dev/null
+++ b/Engine/src/core/java/dev/euph/engine/core/data/octree/Octree.java
@@ -0,0 +1,20 @@
+package dev.euph.engine.core.data.octree;
+
+import lombok.AllArgsConstructor;
+import org.joml.Vector3f;
+
+import java.util.List;
+
+@AllArgsConstructor
+public class Octree {
+ private final OctreeNode root;
+
+ public void insert(Vector3f position, T data) {
+ root.insert(position, data);
+ }
+
+ public List query(Vector3f position, float radius) {
+ return root.query(position, radius);
+ }
+}
+
diff --git a/Engine/src/core/java/dev/euph/engine/core/data/octree/OctreeNode.java b/Engine/src/core/java/dev/euph/engine/core/data/octree/OctreeNode.java
new file mode 100644
index 0000000..af5630c
--- /dev/null
+++ b/Engine/src/core/java/dev/euph/engine/core/data/octree/OctreeNode.java
@@ -0,0 +1,137 @@
+package dev.euph.engine.core.data.octree;
+
+import lombok.Getter;
+import org.joml.Vector3f;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class OctreeNode {
+ private final int maxCapacity;
+ private final Vector3f center;
+ private final float halfSize;
+ private final List positions;
+ private final Map dataMap;
+ @Getter
+ private OctreeNode[] children;
+
+ public OctreeNode(int maxCapacity, Vector3f center, float halfSize) {
+ this.maxCapacity = maxCapacity;
+ this.center = center;
+ this.halfSize = halfSize;
+ this.positions = new ArrayList<>();
+ this.dataMap = new HashMap<>();
+ this.children = null;
+ }
+
+ public void insert(Vector3f position, T data) {
+ if (isOutOfBounds(position)) {
+ return;
+ }
+
+ if (children == null) {
+ positions.add(position);
+ dataMap.put(position, data);
+ if (positions.size() > maxCapacity) {
+ subdivide();
+ }
+ return;
+ }
+
+ for (OctreeNode child : children) {
+ child.insert(position, data);
+ }
+ }
+
+ public List query(Vector3f position, float radius) {
+ List result = new ArrayList<>();
+
+ if (isOutOfBounds(position)) {
+ return result;
+ }
+
+ for (Vector3f itemPosition : positions) {
+ if (itemPosition.distance(position) <= radius) {
+ result.add(dataMap.get(itemPosition));
+ }
+ }
+
+ if (children == null) {
+ return result;
+ }
+ for (OctreeNode child : children) {
+ result.addAll(child.query(position, radius));
+ }
+ return result;
+
+ }
+
+ private boolean isOutOfBounds(Vector3f position) {
+ float minX = center.x - halfSize;
+ float maxX = center.x + halfSize;
+ float minY = center.y - halfSize;
+ float maxY = center.y + halfSize;
+ float minZ = center.z - halfSize;
+ float maxZ = center.z + halfSize;
+
+ return (
+ position.x < minX ||
+ position.x > maxX ||
+ position.y < minY ||
+ position.y > maxY ||
+ position.z < minZ ||
+ position.z > maxZ
+ );
+ }
+
+
+ private void subdivide() {
+ OctreeNode[] childNodes = new OctreeNode[8];
+
+ float quarterSize = halfSize / 2.0f;
+
+ Vector3f[] childCenters = new Vector3f[]{
+ new Vector3f(center.x - quarterSize, center.y - quarterSize, center.z - quarterSize),
+ new Vector3f(center.x + quarterSize, center.y - quarterSize, center.z - quarterSize),
+ new Vector3f(center.x - quarterSize, center.y + quarterSize, center.z - quarterSize),
+ new Vector3f(center.x + quarterSize, center.y + quarterSize, center.z - quarterSize),
+ new Vector3f(center.x - quarterSize, center.y - quarterSize, center.z + quarterSize),
+ new Vector3f(center.x + quarterSize, center.y - quarterSize, center.z + quarterSize),
+ new Vector3f(center.x - quarterSize, center.y + quarterSize, center.z + quarterSize),
+ new Vector3f(center.x + quarterSize, center.y + quarterSize, center.z + quarterSize)
+ };
+
+ for (int i = 0; i < 8; i++) {
+ childNodes[i] = new OctreeNode<>(maxCapacity, childCenters[i], quarterSize);
+ }
+ children = childNodes;
+
+ for (int i = positions.size() - 1; i >= 0; i--) {
+ Vector3f position = positions.get(i);
+ T data = dataMap.remove(position);
+ for (OctreeNode child : children) {
+ if (!child.isOutOfBounds(position)) {
+ child.insert(position, data);
+ break;
+ }
+ }
+
+ positions.remove(i);
+ }
+ }
+
+ public List getPositions() {
+ List allPositions = new ArrayList<>(positions);
+
+ if (children != null) {
+ for (OctreeNode child : children) {
+ allPositions.addAll(child.getPositions());
+ }
+ }
+
+ return allPositions;
+ }
+
+}
diff --git a/Engine/src/core/java/dev/euph/engine/core/data/pipeline/Pipeline.java b/Engine/src/core/java/dev/euph/engine/core/data/pipeline/Pipeline.java
new file mode 100644
index 0000000..4f3d74a
--- /dev/null
+++ b/Engine/src/core/java/dev/euph/engine/core/data/pipeline/Pipeline.java
@@ -0,0 +1,45 @@
+package dev.euph.engine.core.data.pipeline;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+public class Pipeline {
+ private final Collection> pipelineStages;
+
+ public Pipeline() {
+ this.pipelineStages = new ArrayList<>();
+ }
+
+ public Pipeline(PipelineStage pipelineStage) {
+ pipelineStages = Collections.singletonList(pipelineStage);
+ }
+
+ private Pipeline(Collection> pipelineStages) {
+ this.pipelineStages = new ArrayList<>(pipelineStages);
+ }
+
+ public Pipeline addStage(PipelineStage