diff --git a/examples/graph-definition.xml b/examples/graph-definition.xml deleted file mode 100644 index 0ca9d9b..0000000 --- a/examples/graph-definition.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/settings.gradle b/settings.gradle index 07223db..e58984f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,4 +2,5 @@ rootProject.name = 'threedom' include 'threedom-kapt' include 'threedom' - +include 'threedom-gl' +include 'threedom-example' diff --git a/threedom-example/build.gradle b/threedom-example/build.gradle new file mode 100644 index 0000000..6ddd520 --- /dev/null +++ b/threedom-example/build.gradle @@ -0,0 +1,67 @@ +import org.gradle.internal.os.OperatingSystem + +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.3.72' + id 'java' + id 'application' +} + +group 'me.eater.threedom' +version '1.0-SNAPSHOT' + +switch (OperatingSystem.current()) { + case OperatingSystem.LINUX: + project.ext.lwjglNatives = "natives-linux" + break + case OperatingSystem.MAC_OS: + project.ext.lwjglNatives = "natives-macos" + break + case OperatingSystem.WINDOWS: + project.ext.lwjglNatives = "natives-windows" + break +} + +repositories { + mavenCentral() +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + implementation platform("org.lwjgl:lwjgl-bom:3.2.3") + + implementation project(":threedom") + implementation project(":threedom-gl") + + implementation "org.lwjgl:lwjgl" + implementation "org.lwjgl:lwjgl-opengl" + implementation "org.lwjgl:lwjgl-glfw" + + implementation 'org.joml:joml:1.9.24' + + runtimeOnly "org.lwjgl:lwjgl::$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-glfw::$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-opengl::$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-stb::$lwjglNatives" +} + +application { + mainClassName = 'me.eater.threedom.example.MainKt' +} + +compileKotlin { + kotlinOptions.jvmTarget = "12" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "12" +} + +java { + sourceCompatibility = JavaVersion.VERSION_12 + targetCompatibility = JavaVersion.VERSION_12 +} + + +run { + + jvmArgs("-javaagent:$projectDir/../tools/lwjglx-debug-1.0.0.jar") +} diff --git a/threedom-example/src/main/kotlin/me/eater/threedom/example/main.kt b/threedom-example/src/main/kotlin/me/eater/threedom/example/main.kt new file mode 100644 index 0000000..0e04d61 --- /dev/null +++ b/threedom-example/src/main/kotlin/me/eater/threedom/example/main.kt @@ -0,0 +1,251 @@ +package me.eater.threedom.example + +import me.eater.threedom.dom.Document +import me.eater.threedom.dom.IDocument +import me.eater.threedom.dom.PlainNode +import me.eater.threedom.dom.createNode +import me.eater.threedom.example.shader.Plain +import me.eater.threedom.example.vertex.Simple +import me.eater.threedom.gl.GL +import me.eater.threedom.gl.ShaderPreProcessor +import me.eater.threedom.gl.dom.PerspectiveCamera +import me.eater.threedom.gl.dom.TriMesh +import me.eater.threedom.gl.texture.Texture +import me.eater.threedom.gl.vertex.VertexArrayObject +import me.eater.threedom.gl.vertex.VertexBuffer +import me.eater.threedom.utils.joml.toFloat +import me.eater.threedom.utils.joml.translation +import me.eater.threedom.utils.joml.vec3 +import org.joml.Vector2f +import org.joml.Vector3f +import org.lwjgl.glfw.GLFW.* +import org.lwjgl.glfw.GLFWErrorCallback +import org.lwjgl.opengl.GL.createCapabilities +import org.lwjgl.opengl.GL11.* +import org.lwjgl.system.MemoryStack.stackPush +import org.lwjgl.system.MemoryUtil.NULL +import java.time.Instant +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin + +fun main() { + GLFWErrorCallback.createPrint(System.err).set() + + if (!glfwInit()) { + println("Unable to initialize GLFW") + return + } + + glfwDefaultWindowHints() + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4) + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 4) + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE) + + val window = glfwCreateWindow(300, 300, "Hello world", NULL, NULL) + if (window == NULL) { + println("Failed creating window") + return + } + + val stack = stackPush() + val pWidth = stack.mallocInt(1) + val pHeight = stack.mallocInt(1) + + // Get the window size passed to glfwCreateWindow + glfwGetWindowSize(window, pWidth, pHeight) + + // Get the resolution of the primary monitor + val vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor()) ?: return + + // Center the window + glfwSetWindowPos( + window, + (vidmode.width() - pWidth.get(0)) / 2, + (vidmode.height() - pHeight.get(0)) / 2 + ) + + + // Make the OpenGL context current + glfwMakeContextCurrent(window) + + var width = pWidth.get(0) + var height = pHeight.get(0) + + // Enable v-sync + glfwSwapInterval(1) + glfwShowWindow(window) + createCapabilities() + + val doc: IDocument = Document() + + val triMesh = doc.createNode() + + val preProcessor = ShaderPreProcessor.createDefault() + + val mesh = listOf( + -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, + 0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, + 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, + 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, + -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, + -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, + + -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, + 0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, + 0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, + 0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, + -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, + -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, + + -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, + -0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, + -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, + -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, + -0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, + -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, + + 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, + 0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, + 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, + 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, + 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, + 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, + + -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, + 0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, + 0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, + 0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, + -0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, + -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, + + -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, + 0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, + 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, + 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, + -0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, + -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f + ) + + val buffer = mesh + .chunked(6) + .map { + Simple().apply { + position = vec3(it[0], it[1], it[2]) + normal = vec3(it[3], it[4], it[5]) + } + } + .let { + VertexBuffer(it) + } + + val VAO = VertexArrayObject { + buffer(Simple::position) + buffer(Simple::normal) + } + + triMesh.mesh = VAO + println("Loading mesh") + println("Loading shader program") + val plainShader = Plain.load(preProcessor) + val lampShader = Plain.lamp(preProcessor) + + val texture = Texture() + texture.load(Simple::class.java.getResourceAsStream("/textures/fujiwara.jpg").readAllBytes()) + plainShader.apply { + objectColor = vec3(1.0, 0.5, 0.0) + lightColor = vec3(1.0, 1.0, 1.0) + } + + triMesh.use(lampShader) + + val empty = doc.createNode() + + doc.addNode(empty) + empty.addNode(triMesh) + + val around = mutableListOf() + + val amount = 3 + val deg = (PI * 2) / amount + for (i in 0 until amount) { + val trimeshDup = triMesh.clone() + trimeshDup.use(plainShader) + around.add(trimeshDup) + triMesh.addNode(trimeshDup) + + trimeshDup.model { + translate(2 * sin(deg * i), 0.0, 2 * cos(deg * i)) + } + } + + val camera = doc.createNode() + doc.addNode(camera) + + glfwSetFramebufferSizeCallback(window) { _, newWidth, newHeight -> + glViewport(0, 0, newWidth, newHeight) + + camera.width = newWidth.toDouble() + camera.height = newHeight.toDouble() + } + + glfwSetKeyCallback(window) { _, key, _, action, _ -> + if ((key == GLFW_KEY_Q || key == GLFW_KEY_ESCAPE) && action == GLFW_RELEASE) + glfwSetWindowShouldClose(window, true) + + + var z = 0.0 + var x = 0.0 + val speed = 0.1 + if (key == GLFW_KEY_UP && action != GLFW_RELEASE) { + z += speed + } + + if (key == GLFW_KEY_DOWN && action != GLFW_RELEASE) { + z -= speed + } + + if (key == GLFW_KEY_LEFT && action != GLFW_RELEASE) { + x += speed + } + + if (key == GLFW_KEY_RIGHT && action != GLFW_RELEASE) { + x -= speed + } + + if (x != 0.0 || z != 0.0) { + camera.model { + translateLocal(x, 0.0, z) + } + } + } + + println("Starting render loop") + glClearColor(0.3f, 0.2f, 0.2f, 1.0f) + glEnable(GL_DEPTH_TEST) + while (!glfwWindowShouldClose(window)) { + glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) + + empty.model { + setTranslation(0.0, 0.0, sin(Instant.now().toEpochMilli().toDouble() / 1000) - 3) + } + + triMesh.model { + rotateY(0.005) + } + + around.forEach { + it.model { + rotateY(-0.005) + } + } + + plainShader.lightPos = triMesh.absolute.translation.toFloat() + plainShader.viewPos = camera.absolute.translation.toFloat() + doc.render(GL, camera) + + glfwSwapBuffers(window) + glfwPollEvents() + } +} + diff --git a/threedom-example/src/main/kotlin/me/eater/threedom/example/shader/Plain.kt b/threedom-example/src/main/kotlin/me/eater/threedom/example/shader/Plain.kt new file mode 100644 index 0000000..ad11ebe --- /dev/null +++ b/threedom-example/src/main/kotlin/me/eater/threedom/example/shader/Plain.kt @@ -0,0 +1,60 @@ +package me.eater.threedom.example.shader + +import me.eater.threedom.gl.Shader +import me.eater.threedom.gl.ShaderPreProcessor +import me.eater.threedom.gl.ShaderProgram + +class Plain private constructor(vertexShader: Shader, fragmentShader: Shader) : + ShaderProgram(vertexShader, fragmentShader) { + + var objectColor by vec3() + var lightColor by vec3() + var lightPos by vec3() + var viewPos by vec3() + + companion object { + fun load(preProcessor: ShaderPreProcessor): Plain { + val vertex = preProcessor.compile( + this::class.java.getResourceAsStream("/shaders/plain/vertex.glsl").readAllBytes() + .toString(Charsets.UTF_8), + Shader.ShaderType.Vertex + ) + + val fragment = preProcessor.compile( + this::class.java.getResourceAsStream("/shaders/plain/fragment.glsl").readAllBytes() + .toString(Charsets.UTF_8), + Shader.ShaderType.Fragment + + ) + return Plain( + vertex, + fragment + ).also { + vertex.delete() + fragment.delete() + } + } + + fun lamp(preProcessor: ShaderPreProcessor): Plain { + val vertex = preProcessor.compile( + this::class.java.getResourceAsStream("/shaders/plain/vertex.glsl").readAllBytes() + .toString(Charsets.UTF_8), + Shader.ShaderType.Vertex + ) + + val fragment = preProcessor.compile( + this::class.java.getResourceAsStream("/shaders/plain/fragment_lamp.glsl").readAllBytes() + .toString(Charsets.UTF_8), + Shader.ShaderType.Fragment + + ) + return Plain( + vertex, + fragment + ).also { + vertex.delete() + fragment.delete() + } + } + } +} diff --git a/threedom-example/src/main/kotlin/me/eater/threedom/example/vertex/Simple.kt b/threedom-example/src/main/kotlin/me/eater/threedom/example/vertex/Simple.kt new file mode 100644 index 0000000..ef1105f --- /dev/null +++ b/threedom-example/src/main/kotlin/me/eater/threedom/example/vertex/Simple.kt @@ -0,0 +1,8 @@ +package me.eater.threedom.example.vertex + +import me.eater.threedom.gl.vertex.VertexData + +class Simple : VertexData() { + var position by vec3() + var normal by vec3() +} diff --git a/threedom-example/src/main/resources/shaders/plain/fragment.glsl b/threedom-example/src/main/resources/shaders/plain/fragment.glsl new file mode 100644 index 0000000..55730ba --- /dev/null +++ b/threedom-example/src/main/resources/shaders/plain/fragment.glsl @@ -0,0 +1,32 @@ +#version 330 core +out vec4 FragColor; + +in vec3 FragPos; +in vec3 Normal; +in vec3 LightPos; // extra in variable, since we need the light position in view space we calculate this in the vertex shader + +uniform vec3 lightColor; +uniform vec3 objectColor; + +void main() +{ + // ambient + float ambientStrength = 0.1; + vec3 ambient = ambientStrength * lightColor; + + // diffuse + vec3 norm = normalize(Normal); + vec3 lightDir = normalize(LightPos - FragPos); + float diff = max(dot(norm, lightDir), 0.0); + vec3 diffuse = diff * lightColor; + + // specular + float specularStrength = 0.5; + vec3 viewDir = normalize(-FragPos); // the viewer is always at (0,0,0) in view-space, so viewDir is (0,0,0) - Position => -Position + vec3 reflectDir = reflect(-lightDir, norm); + float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32); + vec3 specular = specularStrength * spec * lightColor; + + vec3 result = (ambient + diffuse + specular) * objectColor; + FragColor = vec4(result, 1.0); +} diff --git a/threedom-example/src/main/resources/shaders/plain/fragment_lamp.glsl b/threedom-example/src/main/resources/shaders/plain/fragment_lamp.glsl new file mode 100644 index 0000000..fbbe624 --- /dev/null +++ b/threedom-example/src/main/resources/shaders/plain/fragment_lamp.glsl @@ -0,0 +1,7 @@ +#version 330 core + +out vec4 FragColor; + +void main() { + FragColor = vec4(1.0); +} diff --git a/threedom-example/src/main/resources/shaders/plain/vertex.glsl b/threedom-example/src/main/resources/shaders/plain/vertex.glsl new file mode 100644 index 0000000..e9c60ad --- /dev/null +++ b/threedom-example/src/main/resources/shaders/plain/vertex.glsl @@ -0,0 +1,21 @@ +#version 330 core +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec3 aNormal; + +out vec3 FragPos; +out vec3 Normal; +out vec3 LightPos; + +uniform vec3 lightPos; // we now define the uniform in the vertex shader and pass the 'view space' lightpos to the fragment shader. lightPos is currently in world space. + +uniform mat4 model; +uniform mat4 view; +uniform mat4 projection; + +void main() +{ + gl_Position = projection * view * model * vec4(aPos, 1.0); + FragPos = vec3(view * model * vec4(aPos, 1.0)); + Normal = mat3(transpose(inverse(view * model))) * aNormal; + LightPos = vec3(view * vec4(lightPos, 1.0)); // Transform world-space light position to view-space light position +} diff --git a/threedom-example/src/main/resources/textures/fujiwara.jpg b/threedom-example/src/main/resources/textures/fujiwara.jpg new file mode 100644 index 0000000..0891122 Binary files /dev/null and b/threedom-example/src/main/resources/textures/fujiwara.jpg differ diff --git a/threedom-gl/build.gradle b/threedom-gl/build.gradle new file mode 100644 index 0000000..08f16e6 --- /dev/null +++ b/threedom-gl/build.gradle @@ -0,0 +1,37 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.3.72' + id 'java' +} + +group 'me.eater.threedom' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + implementation platform("org.lwjgl:lwjgl-bom:3.2.3") + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + implementation "org.jetbrains.kotlin:kotlin-reflect" + implementation project(":threedom") + + implementation 'org.joml:joml:1.9.24' + implementation "org.lwjgl:lwjgl" + implementation "org.lwjgl:lwjgl-opengl" + implementation "org.lwjgl:lwjgl-stb" +} + +compileKotlin { + kotlinOptions.jvmTarget = "12" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "12" +} + +java { + sourceCompatibility = JavaVersion.VERSION_12 + targetCompatibility = JavaVersion.VERSION_12 +} + diff --git a/threedom-gl/src/main/kotlin/me/eater/threedom/gl/GL.kt b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/GL.kt new file mode 100644 index 0000000..35b7b7b --- /dev/null +++ b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/GL.kt @@ -0,0 +1,11 @@ +package me.eater.threedom.gl + +import me.eater.threedom.dom.render.IRenderTarget +import me.eater.threedom.gl.dom.ICamera +import org.lwjgl.system.MemoryStack +import org.lwjgl.system.MemoryStack.stackPush + +object GL : IRenderTarget> { + override val type = "GL" + val stack: MemoryStack = stackPush() +} diff --git a/threedom-gl/src/main/kotlin/me/eater/threedom/gl/Shader.kt b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/Shader.kt new file mode 100644 index 0000000..46f4902 --- /dev/null +++ b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/Shader.kt @@ -0,0 +1,32 @@ +package me.eater.threedom.gl + +import me.eater.threedom.gl.GL.stack +import me.eater.threedom.gl.exception.ShaderCompilationException +import org.lwjgl.opengl.GL20.* +import org.lwjgl.system.MemoryUtil + +data class Shader(val shaderId: Int, val shaderType: ShaderType) { + enum class ShaderType(val glId: Int) { + Fragment(GL_FRAGMENT_SHADER), + Vertex(GL_VERTEX_SHADER); + } + + fun delete() { + glDeleteShader(shaderId) + } + + companion object { + fun create(source: String, shaderType: ShaderType): Shader { + val shaderId = glCreateShader(shaderType.glId) + glShaderSource(shaderId, source) + glCompileShader(shaderId) + val success = stack.mallocInt(1) + glGetShaderiv(shaderId, GL_COMPILE_STATUS, success) + if (success.get(0) == 0) { + throw ShaderCompilationException(glGetShaderInfoLog(shaderId)) + } + + return Shader(shaderId, shaderType) + } + } +} diff --git a/threedom-gl/src/main/kotlin/me/eater/threedom/gl/ShaderPreProcessor.kt b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/ShaderPreProcessor.kt new file mode 100644 index 0000000..e80f406 --- /dev/null +++ b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/ShaderPreProcessor.kt @@ -0,0 +1,46 @@ +package me.eater.threedom.gl + +class ShaderPreProcessor { + private val regex = Regex("(?[^:]+)::(?[^:(]+)\\((?.+)\\)") + private val calls: MutableMap, (List) -> String> = mutableMapOf() + + fun process(input: String): String = + input.split("\n").joinToString("\n") { + if (it.startsWith("#[") && it.endsWith("]")) { + val macro = it.substring(2 until it.length - 1) + val match = regex.find(macro) ?: return@joinToString it + val module = match.groups["module"]!!.value + val call = match.groups["call"]!!.value + val args = match.groups["args"]!!.value.split(",").map { arg -> arg.trim() }.toList() + + calls[module to call]?.invoke(args) ?: throw RuntimeException("Can't find macro $module::$call") + } else { + it + } + } + + fun register(module: String, call: String, block: (List) -> String) { + calls[module to call] = block + } + + fun compile(source: String, type: Shader.ShaderType): Shader { + return Shader.create(process(source), type) + } + + companion object { + fun createDefault() = ShaderPreProcessor().apply { + register("3dom", "import") { it -> + val knownUniforms = mapOf( + "model" to "mat4", + "view" to "mat4", + "projection" to "mat4", + "normalModel" to "mat3" + ) + + it.joinToString("\n") { uni -> + "uniform ${knownUniforms[uni]} $uni;" + } + } + } + } +} diff --git a/threedom-gl/src/main/kotlin/me/eater/threedom/gl/ShaderProgram.kt b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/ShaderProgram.kt new file mode 100644 index 0000000..7ade9a3 --- /dev/null +++ b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/ShaderProgram.kt @@ -0,0 +1,220 @@ +package me.eater.threedom.gl + +import me.eater.threedom.gl.GL.stack +import me.eater.threedom.gl.dom.ICamera +import me.eater.threedom.gl.exception.ShaderProgramLinkingException +import me.eater.threedom.gl.texture.Texture +import me.eater.threedom.utils.joml.toFloat +import org.joml.* +import org.lwjgl.opengl.GL20.* +import org.lwjgl.system.MemoryUtil +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +open class ShaderProgram(private val programId: Int = glCreateProgram()) { + private val uniforms: MutableMap = mutableMapOf() + val queuedUniformValues: MutableMap Unit> = mutableMapOf() + private val textures: MutableMap = mutableMapOf() + private var textureIndex: Int = 0 + + var model by mat4() + var normalModel by mat3() + var view by mat4() + var projection by mat4() + + private fun getUniformLocation(name: String) = uniforms.getOrPut(name) { + glGetUniformLocation(programId, name) + } + + protected fun sampler2D( + index: Int = textureIndex++, + name: String? = null + ): ReadWriteProperty { + var setUniform = false + + return object : ReadWriteProperty { + override fun getValue(thisRef: ShaderProgram, property: KProperty<*>): Texture? { + return Texture.getTexture(textures[index] ?: return null) + } + + override fun setValue(thisRef: ShaderProgram, property: KProperty<*>, value: Texture?) { + textures[index] = value?.id ?: 0 + + if (setUniform) { + return + } + + val loc = getUniformLocation(name ?: property.name) + if (loc == -1) { + return + } + + if (glGetInteger(GL_CURRENT_PROGRAM) == thisRef.programId) { + glUniform1i(loc, index) + } else { + queuedUniformValues[loc] = { + glUniform1i(loc, index) + } + } + + setUniform = true + } + } + } + + protected fun uniform( + name: String? = null, + setter: (location: Int, value: T) -> Unit, + getter: (programId: Int, location: Int) -> T? + ) = UniformProperty(name, setter, getter) + + open class UniformProperty( + val name: String? = null, + val setter: (location: Int, value: T) -> Unit, + val getter: (programId: Int, location: Int) -> T? + ) { + operator fun getValue(thisRef: ShaderProgram, property: KProperty<*>): T? { + val loc = thisRef.getUniformLocation(name ?: property.name) + if (loc == -1) { + return null + } + + return getter(thisRef.programId, loc) + } + + operator fun setValue(thisRef: ShaderProgram, property: KProperty<*>, value: T?) { + val valueNonNull = value ?: return + + val loc = thisRef.getUniformLocation(name ?: property.name) + if (loc == -1) { + return + } + + + if (glGetInteger(GL_CURRENT_PROGRAM) == thisRef.programId) { + setter(loc, valueNonNull) + } else { + thisRef.queuedUniformValues[loc] = { + setter(loc, valueNonNull) + } + } + } + } + + protected fun float(name: String? = null): UniformProperty = + uniform(name, ::glUniform1f, ::glGetUniformf) + + protected fun int(name: String? = null): UniformProperty = + uniform(name, ::glUniform1i, ::glGetUniformi) + + protected fun vec2(name: String? = null): UniformProperty = + uniform(name, { loc, value -> + val floatArray = MemoryUtil.memAllocFloat(2) + value.get(floatArray) + glUniform2fv(loc, floatArray) + MemoryUtil.memFree(floatArray) + }, { programId, location -> + val buffer = FloatArray(2) + glGetUniformfv(programId, location, buffer) + Vector2f().set(buffer) + }) + + protected fun vec3(name: String? = null): UniformProperty = + uniform(name, { loc, value -> + val floatArray = MemoryUtil.memAllocFloat(3) + value.get(floatArray) + glUniform3fv(loc, floatArray) + MemoryUtil.memFree(floatArray) + }, { programId, location -> + val buffer = FloatArray(3) + glGetUniformfv(programId, location, buffer) + Vector3f().set(buffer) + }) + + protected fun vec4(name: String? = null): UniformProperty = + uniform(name, { loc, value -> + val floatArray = MemoryUtil.memAllocFloat(4) + value.get(floatArray) + glUniform4fv(loc, floatArray) + MemoryUtil.memFree(floatArray) + }, { programId, location -> + val buffer = FloatArray(4) + glGetUniformfv(programId, location, buffer) + Vector4f().set(buffer) + }) + + protected fun mat3(name: String? = null): UniformProperty = + uniform(name, { loc, value -> + val floatArray = FloatArray(3 * 3) + value.get(floatArray) + glUniformMatrix3fv(loc, false, floatArray) + }, { programId, location -> + val buffer = FloatArray(3 * 3) + glGetUniformfv(programId, location, buffer) + Matrix3f().set(buffer) + }) + + protected fun bool(name: String? = null): UniformProperty = + uniform(name, { loc, value -> + glUniform1i(loc, if (value) 1 else 0) + }, { programId, location -> + glGetUniformi(programId, location) == 1 + }) + + protected fun mat4(name: String? = null): UniformProperty = + uniform(name, { loc, value -> + val floatArray = FloatArray(4 * 4) + value.get(floatArray) + glUniformMatrix4fv(loc, false, floatArray) + }, { programId, location -> + val buffer = FloatArray(4 * 4) + glGetUniformfv(programId, location, buffer) + Matrix4f().set(buffer) + }) + + fun enable() { + glUseProgram(programId) + + for ((_, queued) in queuedUniformValues) { + queued() + } + + for ((index, texture) in textures) { + glActiveTexture(GL_TEXTURE0 + index) + glBindTexture(GL_TEXTURE_2D, texture) + } + + queuedUniformValues.clear() + } + + fun delete() { + glDeleteProgram(programId) + } + + fun disable() { + glUseProgram(0) + } + + fun uses(name: String): Boolean { + return getUniformLocation(name) != -1 + } + + + + fun camera(renderer: ICamera<*>) { + this.projection = renderer.projection.toFloat() + this.view = renderer.view.toFloat() + } + + constructor(vertexShader: Shader, fragmentShader: Shader) : this() { + glAttachShader(programId, vertexShader.shaderId) + glAttachShader(programId, fragmentShader.shaderId) + + glLinkProgram(programId) + val success = stack.mallocInt(1) + glGetProgramiv(programId, GL_LINK_STATUS, success) + if (success.get(0) == 0) { + throw ShaderProgramLinkingException(glGetProgramInfoLog(programId)) + } + } +} diff --git a/threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/GLRenderNode.kt b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/GLRenderNode.kt new file mode 100644 index 0000000..0de2a39 --- /dev/null +++ b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/GLRenderNode.kt @@ -0,0 +1,10 @@ +package me.eater.threedom.gl.dom + +import me.eater.threedom.dom.render.IRenderNode +import me.eater.threedom.gl.GL + +interface GLRenderNode>> : + IRenderNode> { + override val target: GL + get() = GL +} diff --git a/threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/ICamera.kt b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/ICamera.kt new file mode 100644 index 0000000..6c00e16 --- /dev/null +++ b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/ICamera.kt @@ -0,0 +1,12 @@ +package me.eater.threedom.gl.dom + +import me.eater.threedom.dom.INode +import org.joml.Matrix4dc + +interface ICamera> : INode { + var width: Double + var height: Double + + val projection: Matrix4dc + val view: Matrix4dc +} diff --git a/threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/PerspectiveCamera.kt b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/PerspectiveCamera.kt new file mode 100644 index 0000000..e5b2edd --- /dev/null +++ b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/PerspectiveCamera.kt @@ -0,0 +1,53 @@ +package me.eater.threedom.gl.dom + +import me.eater.threedom.dom.IDocument +import me.eater.threedom.dom.Node +import org.joml.Matrix4d +import org.joml.Matrix4dc + +class PerspectiveCamera(document: IDocument?) : Node(document), ICamera { + override fun cloneSelf(): PerspectiveCamera = + PerspectiveCamera(document) + + var fieldOfView: Double = 45.0 + set(value) { + field = value + updateProjection() + } + + var far: Double = 100.0 + set(value) { + field = value + updateProjection() + } + + var near: Double = 0.1 + set(value) { + field = value + updateProjection() + } + + override var width: Double = 800.0 + set(value) { + field = value + updateProjection() + } + + override var height: Double = 600.0 + set(value) { + field = value + updateProjection() + } + + private fun updateProjection() { + projection = makeProjection() + } + + private fun makeProjection(): Matrix4dc = Matrix4d().perspective(fieldOfView, width / height, near, far) + + override var projection: Matrix4dc = makeProjection() + private set + + override val view: Matrix4dc + get() = absolute +} diff --git a/threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/TriMesh.kt b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/TriMesh.kt new file mode 100644 index 0000000..ae99605 --- /dev/null +++ b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/dom/TriMesh.kt @@ -0,0 +1,49 @@ +package me.eater.threedom.gl.dom + +import me.eater.threedom.dom.IDocument +import me.eater.threedom.dom.Node +import me.eater.threedom.gl.ShaderProgram +import me.eater.threedom.gl.vertex.VertexArrayObject +import me.eater.threedom.utils.joml.toFloat +import org.joml.Matrix3d +import org.joml.Matrix4d +import org.lwjgl.opengl.GL30.* + +open class TriMesh(document: IDocument?) : Node(document), GLRenderNode { + var mesh: VertexArrayObject? = null + var program: ShaderProgram? = null + private set + var programConfig: (ShaderProgram.(TriMesh) -> Unit)? = null + private set + + fun use(program: T, block: T.(TriMesh) -> Unit) { + this.program = program + @kotlin.Suppress("UNCHECKED_CAST") + this.programConfig = block as ShaderProgram.(TriMesh) -> Unit + } + + fun use(program: T) { + this.program = program + this.programConfig = null + } + + override fun cloneSelf(): TriMesh { + return TriMesh(document).also { + it.mesh = mesh + it.program = program + it.programConfig = programConfig + } + } + + override fun render(renderer: ICamera<*>) { + val mesh = mesh ?: return + val program = program ?: return + program.enable() + programConfig?.let { program.it(this) } + program.camera(renderer) + program.model = absolute.toFloat() + program.normalModel = model.invert(Matrix4d()).transpose().get3x3(Matrix3d()).toFloat() + glBindVertexArray(mesh.id) + glDrawArrays(GL_TRIANGLES, 0, mesh.size) + } +} diff --git a/threedom-gl/src/main/kotlin/me/eater/threedom/gl/exception/ShaderCompilationException.kt b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/exception/ShaderCompilationException.kt new file mode 100644 index 0000000..37b22d1 --- /dev/null +++ b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/exception/ShaderCompilationException.kt @@ -0,0 +1,3 @@ +package me.eater.threedom.gl.exception + +data class ShaderCompilationException(val infoLog: String) : RuntimeException("Shader failed compiling: $infoLog") diff --git a/threedom-gl/src/main/kotlin/me/eater/threedom/gl/exception/ShaderProgramLinkingException.kt b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/exception/ShaderProgramLinkingException.kt new file mode 100644 index 0000000..2838ede --- /dev/null +++ b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/exception/ShaderProgramLinkingException.kt @@ -0,0 +1,4 @@ +package me.eater.threedom.gl.exception + +data class ShaderProgramLinkingException(val infoLog: String) : + RuntimeException("Failed linking ShaderProgram: $infoLog") diff --git a/threedom-gl/src/main/kotlin/me/eater/threedom/gl/geometry/Plane.kt b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/geometry/Plane.kt new file mode 100644 index 0000000..6e4cc27 --- /dev/null +++ b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/geometry/Plane.kt @@ -0,0 +1,8 @@ +package me.eater.threedom.gl.geometry + +import me.eater.threedom.dom.IDocument +import me.eater.threedom.dom.INode +import me.eater.threedom.gl.dom.TriMesh + +class Plane(document: IDocument?) : TriMesh(document) { +} diff --git a/threedom-gl/src/main/kotlin/me/eater/threedom/gl/texture/Texture.kt b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/texture/Texture.kt new file mode 100644 index 0000000..2a03363 --- /dev/null +++ b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/texture/Texture.kt @@ -0,0 +1,55 @@ +package me.eater.threedom.gl.texture + +import me.eater.threedom.gl.GL.stack +import org.lwjgl.opengl.GL30.* +import org.lwjgl.stb.STBImage.stbi_load_from_memory +import org.lwjgl.stb.STBImage.stbi_set_flip_vertically_on_load +import org.lwjgl.system.MemoryUtil +import java.lang.ref.WeakReference +import java.nio.ByteBuffer + +class Texture { + var id: Int = run { + val pTexture = stack.mallocInt(1) + glGenTextures(pTexture) + pTexture.get(0).also { + textures[it] = WeakReference(this) + } + } + + fun load(image: ByteArray, flipped: Boolean = true, mipmapLevel: Int = 0) { + val mem = MemoryUtil.memAlloc(image.size) + mem.put(image) + mem.rewind() + load(mem, flipped, mipmapLevel) + MemoryUtil.memFree(mem) + } + + fun load(image: ByteBuffer, flipped: Boolean = true, mipmapLevel: Int = 0) { + stbi_set_flip_vertically_on_load(flipped) + val pWidth = stack.mallocInt(1) + val pHeight = stack.mallocInt(1) + val pChannels = stack.mallocInt(1) + val imagePixelData = stbi_load_from_memory(image, pWidth, pHeight, pChannels, 4) + glBindTexture(GL_TEXTURE_2D, id) + glTexImage2D( + GL_TEXTURE_2D, + mipmapLevel, + GL_RGBA8, + pWidth.get(0), + pHeight.get(0), + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + imagePixelData + ) + glGenerateMipmap(GL_TEXTURE_2D) + glBindTexture(GL_TEXTURE_2D, 0) + } + + companion object { + private val textures = mutableMapOf>() + + fun getTexture(id: Int): Texture? = textures[id]?.get() + } +} diff --git a/threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexArrayObject.kt b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexArrayObject.kt new file mode 100644 index 0000000..3f8c3cf --- /dev/null +++ b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexArrayObject.kt @@ -0,0 +1,78 @@ +package me.eater.threedom.gl.vertex + +import me.eater.threedom.gl.GL.stack +import org.lwjgl.opengl.GL30.* +import kotlin.math.min +import kotlin.reflect.KProperty1 + +class VertexArrayObject() { + var size: Int = 0 + + val id = run { + val pId = stack.mallocInt(1) + glGenVertexArrays(pId) + pId.get(0) + } + + constructor(block: VertexArrayObject.() -> Unit) : this() { + block(this) + } + + var pointerIndex: Int = 0 + + operator fun VertexBuffer.invoke(prop: KProperty1) = + bind(this, prop) + + fun bind(vertexBuffer: VertexBuffer, prop: KProperty1) { + if (vertexBuffer.items.isEmpty()) { + return + } + + size = if (pointerIndex == 0) { + vertexBuffer.items.size + } else { + min(size, vertexBuffer.items.size) + } + + val first = vertexBuffer.items.first() + val item = first.getDataPoint(prop) ?: return + var size = item.slots + var length = item.length + var repeat = 1 + + var i = 4 + while (size > 4) { + if ((size % i) == 0) { + repeat = size / i + size = i + length /= i + break + } + + i-- + } + + val stride = if (vertexBuffer.interleaved) { + vertexBuffer.stride + } else { + item.length + } + + var offset = first.getOffset(prop)!! + + if (!vertexBuffer.interleaved) { + offset *= vertexBuffer.items.size + } + + glBindVertexArray(id) + glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer.id) + for (x in 0 until repeat) { + val pointer = pointerIndex++ + glVertexAttribPointer(pointer, size, item.type, false, stride, offset + (x * length).toLong()) + glEnableVertexAttribArray(pointer) + } + + glBindBuffer(GL_ARRAY_BUFFER, 0) + glBindVertexArray(0) + } +} diff --git a/threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexBuffer.kt b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexBuffer.kt new file mode 100644 index 0000000..b129803 --- /dev/null +++ b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexBuffer.kt @@ -0,0 +1,45 @@ +package me.eater.threedom.gl.vertex + +import me.eater.threedom.gl.GL.stack +import org.lwjgl.opengl.GL30.* +import org.lwjgl.system.MemoryUtil +import java.nio.ByteBuffer + +open class VertexBuffer( + items: List = listOf(), + val interleaved: Boolean = false, + val usage: Int = GL_STATIC_DRAW +) { + val id: Int = run { + val pId = stack.mallocInt(1) + glGenBuffers(pId) + pId.get(0) + } + + @Suppress("CanBePrimaryConstructorProperty") + open val items: List = items + var stride: Int + private set + + init { + val size = items.size * (items.firstOrNull()?.length ?: 0).also { stride = it } + val byteBuffer = MemoryUtil.memAlloc(size) + + var offset = 0 + for (i in items.indices) { + val item = items[i] + + if (interleaved) { + item.write(offset, byteBuffer) + offset += item.length + } else { + item.writeNonInterleaved(i, items.size, byteBuffer) + } + } + + glBindBuffer(GL_ARRAY_BUFFER, id) + glBufferData(GL_ARRAY_BUFFER, byteBuffer, usage) + glBindBuffer(GL_ARRAY_BUFFER, 0) + MemoryUtil.memFree(byteBuffer) + } +} diff --git a/threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexBufferMutable.kt b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexBufferMutable.kt new file mode 100644 index 0000000..a4cc90f --- /dev/null +++ b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexBufferMutable.kt @@ -0,0 +1,3 @@ +package me.eater.threedom.gl.vertex + +class VertexBufferMutable(override val items: MutableList = mutableListOf()) : VertexBuffer() diff --git a/threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexData.kt b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexData.kt new file mode 100644 index 0000000..14c00b3 --- /dev/null +++ b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexData.kt @@ -0,0 +1,56 @@ +package me.eater.threedom.gl.vertex + +import me.eater.threedom.gl.vertex.VertexDataPoint.* +import org.joml.* +import java.nio.ByteBuffer +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty1 + +abstract class VertexData() { + private val items = linkedMapOf>>() + private var offset = 0 + + val length + get() = items.map { (_, v) -> v.component2().length }.sum() + + class VertexDataRegister(private val default: T, val provider: (default: T) -> VertexDataPoint) { + operator fun provideDelegate(thisRef: VertexData, prop: KProperty<*>): VertexDataPoint { + val delegate = provider(default) + thisRef.items[prop.name] = thisRef.offset to delegate + thisRef.offset += delegate.length + return delegate + } + } + + fun int(value: Int = 0): VertexDataRegister = VertexDataRegister(value, ::SingleInt) + fun double(value: Double = 0.0): VertexDataRegister = VertexDataRegister(value, ::SingleDouble) + fun float(value: Float = 0f): VertexDataRegister = VertexDataRegister(value, ::SingleFloat) + fun bool(value: Boolean = false): VertexDataRegister = VertexDataRegister(value, ::SingleBoolean) + fun vec2(value: Vector2fc = Vector2f()): VertexDataRegister = VertexDataRegister(value, ::Vec2) + fun vec3(value: Vector3fc = Vector3f()): VertexDataRegister = VertexDataRegister(value, ::Vec3) + fun vec4(value: Vector4fc = Vector4f()): VertexDataRegister = VertexDataRegister(value, ::Vec4) + fun mat3(value: Matrix3fc = Matrix3f()): VertexDataRegister = VertexDataRegister(value, ::Mat3) + fun mat4(value: Matrix4fc = Matrix4f()): VertexDataRegister = VertexDataRegister(value, ::Mat4) + + fun getDataPoint(prop: KProperty1): VertexDataPoint<*>? = items[prop.name]?.component2() + + fun getOffset(prop: KProperty1): Int? = items[prop.name]?.component1() + + fun writeNonInterleaved(position: Int, size: Int, byteBuffer: ByteBuffer) { + var offset = 0; + for ((_, pair) in items) { + val (_, item) = pair + item.write(offset + (position * item.length), byteBuffer) + offset += item.length * size + } + } + + fun write(position: Int, byteBuffer: ByteBuffer) { + var pos = position + for ((_, pair) in items) { + val (_, item) = pair + item.write(pos, byteBuffer) + pos += item.length + } + } +} diff --git a/threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexDataPoint.kt b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexDataPoint.kt new file mode 100644 index 0000000..140fd1e --- /dev/null +++ b/threedom-gl/src/main/kotlin/me/eater/threedom/gl/vertex/VertexDataPoint.kt @@ -0,0 +1,73 @@ +package me.eater.threedom.gl.vertex + +import org.joml.* +import org.lwjgl.opengl.GL30.* +import java.nio.ByteBuffer +import kotlin.reflect.KProperty + +abstract class VertexDataPoint(val type: Int, var value: T, val length: Int, val slots: Int = 1) { + abstract fun write(position: Int, buffer: ByteBuffer) + + operator fun getValue(thisRef: Any?, property: KProperty<*>): T { + return value + } + + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + this.value = value + } + + class SingleInt(value: Int) : VertexDataPoint(GL_INT, value, 4) { + override fun write(position: Int, buffer: ByteBuffer) { + buffer.putInt(position, value) + } + + } + + class SingleFloat(value: Float) : VertexDataPoint(GL_FLOAT, value, 4) { + override fun write(position: Int, buffer: ByteBuffer) { + buffer.putFloat(position, value) + } + } + + class SingleDouble(value: Double) : VertexDataPoint(GL_DOUBLE, value, 8) { + override fun write(position: Int, buffer: ByteBuffer) { + buffer.putDouble(position, value) + } + } + + class SingleBoolean(value: Boolean) : VertexDataPoint(GL_BOOL, value, 1) { + override fun write(position: Int, buffer: ByteBuffer) { + buffer.put(position, if (value) 1 else 2) + } + } + + class Vec2(value: Vector2fc) : VertexDataPoint(GL_FLOAT, value, 8, 2) { + override fun write(position: Int, buffer: ByteBuffer) { + value.get(position, buffer) + } + } + + class Vec3(value: Vector3fc) : VertexDataPoint(GL_FLOAT, value, 12, 3) { + override fun write(position: Int, buffer: ByteBuffer) { + value.get(position, buffer) + } + } + + class Vec4(value: Vector4fc) : VertexDataPoint(GL_FLOAT, value, 16, 4) { + override fun write(position: Int, buffer: ByteBuffer) { + value.get(position, buffer) + } + } + + class Mat3(value: Matrix3fc) : VertexDataPoint(GL_FLOAT, value, 3 * 3 * 4, 9) { + override fun write(position: Int, buffer: ByteBuffer) { + value.get(position, buffer) + } + } + + class Mat4(value: Matrix4fc) : VertexDataPoint(GL_FLOAT, value, 4 * 4 * 4, 16) { + override fun write(position: Int, buffer: ByteBuffer) { + value.get(position, buffer) + } + } +} diff --git a/threedom-kapt/build.gradle b/threedom-kapt/build.gradle index e930f3e..dbbb5b9 100644 --- a/threedom-kapt/build.gradle +++ b/threedom-kapt/build.gradle @@ -1,5 +1,6 @@ plugins { id 'org.jetbrains.kotlin.jvm' version '1.3.72' + id 'java' } group 'me.eater.threedom' @@ -11,8 +12,13 @@ dependencies { } compileKotlin { - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = "12" } compileTestKotlin { - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = "12" +} + +java { + sourceCompatibility = JavaVersion.VERSION_12 + targetCompatibility = JavaVersion.VERSION_12 } diff --git a/threedom/build.gradle b/threedom/build.gradle index 261bfd0..6999a06 100644 --- a/threedom/build.gradle +++ b/threedom/build.gradle @@ -1,6 +1,7 @@ plugins { id 'org.jetbrains.kotlin.jvm' version '1.3.72' id "org.jetbrains.kotlin.kapt" version "1.3.72" + id 'java' } group 'me.eater.threedom' @@ -30,8 +31,13 @@ dependencies { } compileKotlin { - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = "12" } compileTestKotlin { - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = "12" +} + +java { + sourceCompatibility = JavaVersion.VERSION_12 + targetCompatibility = JavaVersion.VERSION_12 } diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/Document.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/Document.kt index eb1528a..8170586 100644 --- a/threedom/src/main/kotlin/me/eater/threedom/dom/Document.kt +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/Document.kt @@ -1,9 +1,8 @@ package me.eater.threedom.dom -import me.eater.threedom.dom.event.DOMTreeUpdate -import me.eater.threedom.dom.event.NodeClassListUpdate -import me.eater.threedom.dom.event.NodeIDUpdate -import me.eater.threedom.dom.event.NodeModelUpdate +import me.eater.threedom.dom.event.* +import me.eater.threedom.dom.render.IRenderNode +import me.eater.threedom.dom.render.IRenderTarget import me.eater.threedom.event.Event import me.eater.threedom.generated.EventNames import me.eater.threedom.utils.KDTree @@ -12,6 +11,7 @@ import kotlin.reflect.KClass class Document : IDocument { val root: PlainNode = PlainNode(this) + private val eventTree = EventTree { on { (event) -> removeNodeFromSearch(event.child) @@ -22,7 +22,9 @@ class Document : IDocument { } on { (event) -> - updateKDTreeForNode(event.child) + if (event.child.shouldIndexLocation) { + updateKDTreeForNode(event.child) + } } on { (event) -> @@ -36,19 +38,19 @@ class Document : IDocument { } on { (event) -> - for (className in event.classNames) { - byClass.getOrPut(className, ::mutableSetOf).add(event.node.nodeId) + for (tagName in event.tagNames) { + byTag.getOrPut(tagName, ::mutableSetOf).add(event.node.nodeId) } } on { (event) -> - for (className in event.classNames) { - val set = byClass[className] ?: continue + for (tagName in event.tagNames) { + val set = byTag[tagName] ?: continue set.remove(event.node.nodeId) if (set.size == 0) { - byClass.remove(className) + byTag.remove(tagName) } } } @@ -56,20 +58,34 @@ class Document : IDocument { on { (event) -> updateKDTreeForNode(event.node) } + + on { (event) -> + if (event.shouldIndex) { + kdTree.add(event.node) + } else { + kdTree.remove(event.node) + } + } } private val kdTree = KDTree(this) private val allNodes: MutableMap> = mutableMapOf() private val byId: MutableMap = mutableMapOf() - private val byClass: MutableMap> = mutableMapOf() + private val byTag: MutableMap> = mutableMapOf() + private val byType: MutableMap> = mutableMapOf() + private val byRenderTarget: MutableMap> = mutableMapOf() private fun updateKDTreeForNode(node: INode<*>) { node.updateAbsolute() - kdTree.update(node) + if (node.shouldIndexLocation) { + kdTree.update(node) + } for (child in node.recursiveIterator()) { child.updateAbsolute() - kdTree.update(child) + if (node.shouldIndexLocation) { + kdTree.update(child) + } } } @@ -107,8 +123,21 @@ class Document : IDocument { override fun getNodeById(id: String): INode<*>? = byId[id]?.let(allNodes::get) - override fun getNodesByClassName(className: String): Sequence> = - byClass[className]?.asSequence()?.mapNotNull(allNodes::get) ?: emptySequence() + override fun getNodesByTag(tagName: String): Sequence> = + byTag[tagName]?.asSequence()?.mapNotNull(allNodes::get) ?: emptySequence() + + @Suppress("UNCHECKED_CAST") + override fun > getNodesByClass(className: String): Sequence = + byType[className]?.asSequence()?.mapNotNull { nodeId -> + allNodes[nodeId]?.let { + (it.className == className) as? T + } + } ?: emptySequence() + + @Suppress("UNCHECKED_CAST") + override fun , C> getNodesByRenderTarget(targetType: String): Sequence> = + byRenderTarget[targetType]?.asSequence()?.mapNotNull(allNodes::get) as? Sequence> + ?: emptySequence() private fun addNodeToSearch(node: INode<*>) { @@ -116,11 +145,19 @@ class Document : IDocument { node.id?.let { byId.putIfAbsent(it, node.nodeId) } - for (className in node.classList) { - byClass.getOrPut(className, ::mutableSetOf).add(node.nodeId) + for (className in node.tagList) { + byTag.getOrPut(className, ::mutableSetOf).add(node.nodeId) + } + + byType.getOrPut(node.javaClass.name, ::mutableSetOf).add(node.nodeId) + + if (node is IRenderNode<*, *, *>) { + byRenderTarget.getOrPut(node.target.type, ::mutableSetOf).add(node.nodeId) } - kdTree.add(node) + if (node.shouldIndexLocation) { + kdTree.add(node) + } } private fun removeNodeFromSearch(node: INode<*>) { @@ -128,15 +165,36 @@ class Document : IDocument { node.id?.let { byId.remove(it, node.nodeId) } - for (className in node.classList) { - val set = byClass.get(className) ?: continue + for (className in node.tagList) { + val set = byTag.get(className) ?: continue set.remove(node.nodeId) if (set.size == 0) { - byClass.remove(className) + byTag.remove(className) + } + } + + + byType[node.javaClass.name]?.let { + it.remove(node.nodeId) + + if (it.size == 0) { + byType.remove(node.javaClass.name) } } - kdTree.remove(node) + if (node is IRenderNode<*, *, *>) { + byRenderTarget[node.target.type]?.let { + it.remove(node.nodeId) + + if (it.size == 0) { + byRenderTarget.remove(node.target.type) + } + } + } + + if (node.shouldIndexLocation) { + kdTree.remove(node) + } } inline fun on(topLevel: Boolean = false, noinline block: (Event) -> Unit) = if (topLevel) diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/IDocument.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/IDocument.kt index 1f8c0b0..fa9ade2 100644 --- a/threedom/src/main/kotlin/me/eater/threedom/dom/IDocument.kt +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/IDocument.kt @@ -1,5 +1,7 @@ package me.eater.threedom.dom +import me.eater.threedom.dom.render.IRenderNode +import me.eater.threedom.dom.render.IRenderTarget import me.eater.threedom.event.Event import me.eater.threedom.event.EventDispatcher import me.eater.threedom.utils.joml.Vector3d @@ -14,6 +16,12 @@ interface IDocument : EventDispatcher, INodeContainer { fun findAt(x: Number, y: Number, z: Number) = findAt(Vector3d(x, y, z)) fun findAt(vec: Vector3dc): Collection> fun rebalance() + + fun , N : IRenderNode<*, T, C>> render(target: T, renderer: C) { + for (node in getNodesByRenderTarget(target.type)) { + node.render(renderer) + } + } } inline fun > IDocument.createNode() = createNode(T::class) diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/INode.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/INode.kt index 5580435..4b97ef9 100644 --- a/threedom/src/main/kotlin/me/eater/threedom/dom/INode.kt +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/INode.kt @@ -6,6 +6,9 @@ import org.joml.Matrix4dc import java.util.concurrent.atomic.AtomicLong interface INode> : Comparable>, INodeContainer { + val className: String + get() = this::class.java.name + /** * The ID of this node */ @@ -14,7 +17,7 @@ interface INode> : Comparable>, INodeContainer { /** * Set with all class names assigned to this node */ - val classList: MutableSet + val tagList: MutableSet /** * Internal ID of this node, should be unique inside document @@ -31,6 +34,12 @@ interface INode> : Comparable>, INodeContainer { */ val document: IDocument? + /** + * If the location should be indexed + */ + val shouldIndexLocation: Boolean + get() = false + /** * Absolute matrix relative to document */ @@ -47,7 +56,7 @@ interface INode> : Comparable>, INodeContainer { * * @param deep also clone all child nodes */ - fun clone(deep: Boolean): T + fun clone(deep: Boolean = false): T /** * Update the parent of this node, for consistency purposes. diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/INodeQueryCapable.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/INodeQueryCapable.kt index 22a0420..8a13484 100644 --- a/threedom/src/main/kotlin/me/eater/threedom/dom/INodeQueryCapable.kt +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/INodeQueryCapable.kt @@ -1,5 +1,7 @@ package me.eater.threedom.dom +import me.eater.threedom.dom.render.IRenderNode +import me.eater.threedom.dom.render.IRenderTarget import org.joml.Vector3dc interface INodeQueryCapable { @@ -9,9 +11,19 @@ interface INodeQueryCapable { fun getNodeById(id: String): INode<*>? /** - * Get all nodes inside this node with the class [className] + * Get all nodes inside this node with the class [tagName] */ - fun getNodesByClassName(className: String): Sequence> + fun getNodesByTag(tagName: String): Sequence> + + /** + * Get all nodes inside this node with the Java class [className] + */ + fun > getNodesByClass(className: String): Sequence + + /** + * Get all nodes inside this node with the render target with type of [targetType]s + */ + fun , C> getNodesByRenderTarget(targetType: String): Sequence> /** * find all nodes inside this node in region between [pointA] and [pointB] diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/Node.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/Node.kt index 5cad9c0..3ae58bd 100644 --- a/threedom/src/main/kotlin/me/eater/threedom/dom/Node.kt +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/Node.kt @@ -1,9 +1,8 @@ package me.eater.threedom.dom -import me.eater.threedom.dom.event.DOMTreeUpdate -import me.eater.threedom.dom.event.NodeClassListUpdate -import me.eater.threedom.dom.event.NodeIDUpdate -import me.eater.threedom.dom.event.NodeModelUpdate +import me.eater.threedom.dom.event.* +import me.eater.threedom.dom.render.IRenderNode +import me.eater.threedom.dom.render.IRenderTarget import me.eater.threedom.event.Event import me.eater.threedom.event.EventListener import me.eater.threedom.event.trigger @@ -15,6 +14,8 @@ import org.joml.Matrix4dc import org.joml.Vector3dc abstract class Node>(document: IDocument?) : INode, EventListener { + override val className: String = this::class.java.name + override var document: IDocument? = document protected set override val nodeId = INode.getNextNodeId() @@ -23,12 +24,18 @@ abstract class Node>(document: IDocument?) : INode, EventListene override var parentNode: INode<*>? = null protected set - private var _id: String? = null - override var id: String? - get() = _id + override var shouldIndexLocation: Boolean = false set(value) { - val old = _id - _id = value + if (field == value) return + + field = value + trigger(NodeLocationIndexStateChange(field, this)) + } + + override var id: String? = null + set(value) { + val old = field + field = value trigger(NodeIDUpdate(this, old, value)) } @@ -36,7 +43,7 @@ abstract class Node>(document: IDocument?) : INode, EventListene override var absolute: Matrix4dc = Matrix4d() protected set - override val classList: MutableSet = ObservableSet { + override val tagList: MutableSet = ObservableSet { when (it.action) { ObservableSet.Action.Removed -> trigger(NodeClassListUpdate.Removed(it.elements, this)) ObservableSet.Action.Added -> trigger(NodeClassListUpdate.Added(it.elements, this)) @@ -49,17 +56,15 @@ abstract class Node>(document: IDocument?) : INode, EventListene } } - private var _model: Matrix4d = Matrix4d() - override var model: Matrix4dc - get() = _model + override var model: Matrix4dc = Matrix4d() set(value) { - val old = Matrix4d(_model) - _model = value.mutable() + val old = Matrix4d(field) + field = value.mutable() trigger(NodeModelUpdate(this, old)) } fun model(block: Matrix4d.() -> Matrix4dc) { - this.model = block(this._model) + this.model = block(this.model.mutable()) } var children: List> = nodes.toList() @@ -190,10 +195,16 @@ abstract class Node>(document: IDocument?) : INode, EventListene } override fun getNodeById(id: String): INode<*>? = document?.getNodeById(id)?.takeIf { it.hasParent(this) } - override fun getNodesByClassName(className: String): Sequence> = if (nodes.isEmpty()) + override fun getNodesByTag(tagName: String): Sequence> = if (nodes.isEmpty()) emptySequence() else - document?.getNodesByClassName(className)?.filter { it.hasParent(this) } ?: emptySequence() + document?.getNodesByTag(tagName)?.filter { it.hasParent(this) } ?: emptySequence() + + override fun > getNodesByClass(className: String): Sequence = + document?.getNodesByClass(className)?.filter { it.hasParent(this) } ?: emptySequence() + + override fun , C> getNodesByRenderTarget(targetType: String): Sequence> = + document?.getNodesByRenderTarget(targetType)?.filter { it.hasParent(this) } ?: emptySequence() override fun updateAbsolute() { this.absolute = (this.parentNode?.absolute ?: Matrix4d()) * model diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/PlainNode.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/PlainNode.kt index 940ef80..d4cc4d2 100644 --- a/threedom/src/main/kotlin/me/eater/threedom/dom/PlainNode.kt +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/PlainNode.kt @@ -1,7 +1,5 @@ package me.eater.threedom.dom class PlainNode(document: IDocument?) : Node(document) { - override fun cloneSelf(): PlainNode { - return PlainNode(document) - } + override fun cloneSelf(): PlainNode = PlainNode(document) } diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/event/NodeClassListUpdate.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/event/NodeClassListUpdate.kt index ead11c6..1bee03d 100644 --- a/threedom/src/main/kotlin/me/eater/threedom/dom/event/NodeClassListUpdate.kt +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/event/NodeClassListUpdate.kt @@ -7,8 +7,8 @@ sealed class NodeClassListUpdate { abstract val node: INode<*> @EventName("NodeClassesRemoved") - class Removed(val classNames: Set, override val node: INode<*>) : NodeClassListUpdate() + class Removed(val tagNames: Set, override val node: INode<*>) : NodeClassListUpdate() @EventName("NodeClassesAdded") - class Added(val classNames: Set, override val node: INode<*>) : NodeClassListUpdate() + class Added(val tagNames: Set, override val node: INode<*>) : NodeClassListUpdate() } diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/event/NodeLocationIndexStateChange.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/event/NodeLocationIndexStateChange.kt new file mode 100644 index 0000000..761af5a --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/event/NodeLocationIndexStateChange.kt @@ -0,0 +1,7 @@ +package me.eater.threedom.dom.event + +import me.eater.threedom.dom.INode +import me.eater.threedom.kapt.EventName + +@EventName("NodeLocationIndexStateChange") +class NodeLocationIndexStateChange(val shouldIndex: Boolean, val node: INode<*>) diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/render/IRenderNode.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/render/IRenderNode.kt new file mode 100644 index 0000000..a0d88d6 --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/render/IRenderNode.kt @@ -0,0 +1,10 @@ +package me.eater.threedom.dom.render + +import me.eater.threedom.dom.INode + +interface IRenderNode, T : IRenderTarget, C> : + INode { + val target: T + + fun render(renderer: C) +} diff --git a/threedom/src/main/kotlin/me/eater/threedom/dom/render/IRenderTarget.kt b/threedom/src/main/kotlin/me/eater/threedom/dom/render/IRenderTarget.kt new file mode 100644 index 0000000..a9e4aba --- /dev/null +++ b/threedom/src/main/kotlin/me/eater/threedom/dom/render/IRenderTarget.kt @@ -0,0 +1,5 @@ +package me.eater.threedom.dom.render + +interface IRenderTarget { + val type: String +} diff --git a/threedom/src/main/kotlin/me/eater/threedom/utils/joml/JOML.kt b/threedom/src/main/kotlin/me/eater/threedom/utils/joml/JOML.kt index b08c35c..4ae544b 100644 --- a/threedom/src/main/kotlin/me/eater/threedom/utils/joml/JOML.kt +++ b/threedom/src/main/kotlin/me/eater/threedom/utils/joml/JOML.kt @@ -1,9 +1,12 @@ package me.eater.threedom.utils.joml -import org.joml.Matrix4d -import org.joml.Matrix4dc +import org.joml.* +import org.joml.Vector2d +import org.joml.Vector2f import org.joml.Vector3d -import org.joml.Vector3dc +import org.joml.Vector3f +import org.joml.Vector4d +import org.joml.Vector4f fun Matrix4dc.setTranslation(x: T, y: T, z: T): Matrix4d = Matrix4d(this).setTranslation(x.toDouble(), y.toDouble(), z.toDouble()) @@ -16,9 +19,61 @@ fun Matrix4dc.mutable(): Matrix4d = if (this is Matrix4d) this else Matrix4d(thi operator fun Matrix4dc.times(rhs: Matrix4dc) = mul(rhs, Matrix4d()) operator fun Matrix4d.times(rhs: Matrix4dc) = mul(rhs) +@Suppress("FunctionName") +fun Vector2f(x: Number, y: Number) = Vector2f(x.toFloat(), y.toFloat()) + +@Suppress("FunctionName") +fun Vector2d(x: Number, y: Number) = Vector2d(x.toDouble(), y.toDouble()) + +fun vec2(x: Number, y: Number) = Vector2f(x, y) +fun vec2d(x: Number, y: Number) = Vector2d(x, y) + +@Suppress("FunctionName") +fun Vector3f(x: Number, y: Number, z: Number) = Vector3f(x.toFloat(), y.toFloat(), z.toFloat()) + @Suppress("FunctionName") fun Vector3d(x: Number, y: Number, z: Number) = Vector3d(x.toDouble(), y.toDouble(), z.toDouble()) +fun vec3(x: Number, y: Number, z: Number) = Vector3f(x, y, z) +fun vec3d(x: Number, y: Number, z: Number) = Vector3d(x, y, z) + +@Suppress("FunctionName") +fun Vector4f(x: Number, y: Number, z: Number, w: Number) = + Vector4f(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat()) + +@Suppress("FunctionName") +fun Vector4d(x: Number, y: Number, z: Number, w: Number) = + Vector4d(x.toDouble(), y.toDouble(), z.toDouble(), w.toDouble()) + +fun vec4(x: Number, y: Number, z: Number, w: Number) = Vector4f(x, y, z, w) +fun vec4d(x: Number, y: Number, z: Number, w: Number) = Vector4d(x, y, z, w) + +fun Vector2d.toFloat() = vec2(x, y) +fun Vector3d.toFloat() = vec3(x, y, z) +fun Vector4d.toFloat() = vec4(x, y, z, w) + +fun Vector2f.toDouble() = vec2d(x, y) +fun Vector3f.toDouble() = vec3d(x, y, z) +fun Vector4f.toDouble() = vec4d(x, y, z, w) + +fun Matrix4dc.toFloat() = Matrix4f(this) +fun Matrix3dc.toFloat() = + Matrix3f( + m00().toFloat(), + m01().toFloat(), + m02().toFloat(), + m10().toFloat(), + m11().toFloat(), + m12().toFloat(), + m20().toFloat(), + m21().toFloat(), + m22().toFloat() + ) + + +fun Matrix2dc.toFloat() = + Matrix2f(this.m00().toFloat(), this.m01().toFloat(), this.m10().toFloat(), this.m11().toFloat()) + operator fun Vector3dc.compareTo(rhs: Vector3dc): Int { for (i in 0 until 3) { val c = this[i].compareTo(rhs[i]) diff --git a/threedom/src/test/kotlin/me/eater/test/threedom/dom/DocumentTest.kt b/threedom/src/test/kotlin/me/eater/test/threedom/dom/DocumentTest.kt index ebda536..12ab980 100644 --- a/threedom/src/test/kotlin/me/eater/test/threedom/dom/DocumentTest.kt +++ b/threedom/src/test/kotlin/me/eater/test/threedom/dom/DocumentTest.kt @@ -109,26 +109,26 @@ class DocumentTest : StringSpec({ triggered++ } - newNode.classList.add(":3") + newNode.tagList.add(":3") doc.addNode(secondNewNode) doc.addNode(thirdNewNode) - doc.getNodesByClassName(":3") shouldHaveCount 0 + doc.getNodesByTag(":3") shouldHaveCount 0 thirdNewNode.addNode(newNode) - doc.getNodesByClassName(":3") shouldHaveSingleElement newNode - secondNewNode.getNodesByClassName(":3") shouldHaveCount 0 - thirdNewNode.getNodesByClassName(":3") shouldHaveSingleElement newNode + doc.getNodesByTag(":3") shouldHaveSingleElement newNode + secondNewNode.getNodesByTag(":3") shouldHaveCount 0 + thirdNewNode.getNodesByTag(":3") shouldHaveSingleElement newNode thirdNewNode.removeAll() - thirdNewNode.getNodesByClassName(":3") shouldHaveCount 0 - doc.getNodesByClassName(":3") shouldHaveCount 0 - newNode.classList.clear() - newNode.classList.add(":/") + thirdNewNode.getNodesByTag(":3") shouldHaveCount 0 + doc.getNodesByTag(":3") shouldHaveCount 0 + newNode.tagList.clear() + newNode.tagList.add(":/") doc.addNode(newNode) - doc.getNodesByClassName(":3") shouldHaveCount 0 - doc.getNodesByClassName(":/") shouldHaveSingleElement newNode - newNode.classList.clear() - newNode.classList.add(":3") - doc.getNodesByClassName(":/") shouldHaveCount 0 - doc.getNodesByClassName(":3") shouldHaveSingleElement newNode + doc.getNodesByTag(":3") shouldHaveCount 0 + doc.getNodesByTag(":/") shouldHaveSingleElement newNode + newNode.tagList.clear() + newNode.tagList.add(":3") + doc.getNodesByTag(":/") shouldHaveCount 0 + doc.getNodesByTag(":3") shouldHaveSingleElement newNode } }) diff --git a/threedom/src/test/kotlin/me/eater/test/threedom/dom/PositionTest.kt b/threedom/src/test/kotlin/me/eater/test/threedom/dom/PositionTest.kt index 6f7b48d..3f32c4a 100644 --- a/threedom/src/test/kotlin/me/eater/test/threedom/dom/PositionTest.kt +++ b/threedom/src/test/kotlin/me/eater/test/threedom/dom/PositionTest.kt @@ -18,9 +18,11 @@ class PositionTest : StringSpec({ val doc: IDocument = Document() val node = doc.createNode() doc.addNode(node) + node.shouldIndexLocation = true node.model { setTranslation(10, 0, 10) } node.absolute.translation shouldBe Vector3d(10, 0, 10) val nodeTwo = doc.createNode() + nodeTwo.shouldIndexLocation = true node.addNode(nodeTwo) nodeTwo.model { setTranslation(-10, 20, 0) } nodeTwo.absolute.translation shouldBe Vector3d(0, 20, 10) @@ -40,6 +42,8 @@ class PositionTest : StringSpec({ nodeTwo.model { setTranslation(-10, 20, 0) } nodeThree.model { setTranslation(0, 20, 0) } + listOf(node, nodeTwo, nodeThree).forEach { it.shouldIndexLocation = true } + doc.findAt(10, 0, 10) shouldHaveSingleElement node doc.findAt(0, 20, 10) shouldHaveSingleElement nodeTwo doc.findAt(0, 40, 10) shouldHaveSingleElement nodeThree @@ -64,6 +68,8 @@ class PositionTest : StringSpec({ nodeTwo.model { setTranslation(-10, 20, 0) } nodeThree.model { setTranslation(0, 20, 0) } + listOf(node, nodeTwo, nodeThree).forEach { it.shouldIndexLocation = true } + val result = doc.findInRegion(Vector3d(0, 0, 0), Vector3d(0, 20, 20)).toList() result shouldHaveSize 1 result shouldHaveSingleElement nodeTwo @@ -78,6 +84,9 @@ class PositionTest : StringSpec({ val node = doc.createNode() val nodeTwo = doc.createNode() val nodeThree = doc.createNode() + + listOf(node, nodeTwo, nodeThree).forEach { it.shouldIndexLocation = true } + doc.addNode(node) node.addNode(nodeTwo) nodeTwo.addNode(nodeThree) diff --git a/tools/lwjglx-debug-1.0.0.jar b/tools/lwjglx-debug-1.0.0.jar new file mode 100644 index 0000000..3b1760d Binary files /dev/null and b/tools/lwjglx-debug-1.0.0.jar differ