add gl and example
continuous-integration/drone/push Build is failing Details

master
eater 5 years ago
parent eff722ea05
commit 7fc4b44571
Signed by: eater
GPG Key ID: AD2560A0F84F0759

@ -1,5 +0,0 @@
<graph>
<box diameter="10" position="[33, 40, 3]">
</box>
</graph>

@ -2,4 +2,5 @@ rootProject.name = 'threedom'
include 'threedom-kapt'
include 'threedom'
include 'threedom-gl'
include 'threedom-example'

@ -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")
}

@ -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<TriMesh>()
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<PlainNode>()
doc.addNode(empty)
empty.addNode(triMesh)
val around = mutableListOf<TriMesh>()
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<PerspectiveCamera>()
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()
}
}

@ -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()
}
}
}
}

@ -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()
}

@ -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);
}

@ -0,0 +1,7 @@
#version 330 core
out vec4 FragColor;
void main() {
FragColor = vec4(1.0);
}

@ -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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

@ -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
}

@ -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<ICamera<*>> {
override val type = "GL"
val stack: MemoryStack = stackPush()
}

@ -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)
}
}
}

@ -0,0 +1,46 @@
package me.eater.threedom.gl
class ShaderPreProcessor {
private val regex = Regex("(?<module>[^:]+)::(?<call>[^:(]+)\\((?<args>.+)\\)")
private val calls: MutableMap<Pair<String, String>, (List<String>) -> 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>) -> 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;"
}
}
}
}
}

@ -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<String, Int> = mutableMapOf()
val queuedUniformValues: MutableMap<Int, () -> Unit> = mutableMapOf()
private val textures: MutableMap<Int, Int> = 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<ShaderProgram, Texture?> {
var setUniform = false
return object : ReadWriteProperty<ShaderProgram, Texture?> {
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 <T> uniform(
name: String? = null,
setter: (location: Int, value: T) -> Unit,
getter: (programId: Int, location: Int) -> T?
) = UniformProperty(name, setter, getter)
open class UniformProperty<T>(
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<Float> =
uniform(name, ::glUniform1f, ::glGetUniformf)
protected fun int(name: String? = null): UniformProperty<Int> =
uniform(name, ::glUniform1i, ::glGetUniformi)
protected fun vec2(name: String? = null): UniformProperty<Vector2fc> =
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<Vector3fc> =
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<Vector4fc> =
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<Matrix3fc> =
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<Boolean> =
uniform(name, { loc, value ->
glUniform1i(loc, if (value) 1 else 0)
}, { programId, location ->
glGetUniformi(programId, location) == 1
})
protected fun mat4(name: String? = null): UniformProperty<Matrix4fc> =
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))
}
}
}

@ -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<T : IRenderNode<T, GL, ICamera<*>>> :
IRenderNode<T, GL, ICamera<*>> {
override val target: GL
get() = GL
}

@ -0,0 +1,12 @@
package me.eater.threedom.gl.dom
import me.eater.threedom.dom.INode
import org.joml.Matrix4dc
interface ICamera<T : INode<T>> : INode<T> {
var width: Double
var height: Double
val projection: Matrix4dc
val view: Matrix4dc
}

@ -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<PerspectiveCamera>(document), ICamera<PerspectiveCamera> {
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
}

@ -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<TriMesh>(document), GLRenderNode<TriMesh> {
var mesh: VertexArrayObject? = null
var program: ShaderProgram? = null
private set
var programConfig: (ShaderProgram.(TriMesh) -> Unit)? = null
private set
fun <T : ShaderProgram> use(program: T, block: T.(TriMesh) -> Unit) {
this.program = program
@kotlin.Suppress("UNCHECKED_CAST")
this.programConfig = block as ShaderProgram.(TriMesh) -> Unit
}
fun <T : ShaderProgram> 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)
}
}

@ -0,0 +1,3 @@
package me.eater.threedom.gl.exception
data class ShaderCompilationException(val infoLog: String) : RuntimeException("Shader failed compiling: $infoLog")

@ -0,0 +1,4 @@
package me.eater.threedom.gl.exception
data class ShaderProgramLinkingException(val infoLog: String) :
RuntimeException("Failed linking ShaderProgram: $infoLog")

@ -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) {
}

@ -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<Int, WeakReference<Texture>>()
fun getTexture(id: Int): Texture? = textures[id]?.get()
}
}

@ -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 <T : VertexData> VertexBuffer<T>.invoke(prop: KProperty1<T, *>) =
bind(this, prop)
fun <T : VertexData> bind(vertexBuffer: VertexBuffer<T>, prop: KProperty1<T, *>) {
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)
}
}

@ -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<T : VertexData>(
items: List<T> = 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<T> = 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)
}
}

@ -0,0 +1,3 @@
package me.eater.threedom.gl.vertex
class VertexBufferMutable<T: VertexData>(override val items: MutableList<T> = mutableListOf()) : VertexBuffer<T>()

@ -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<String, Pair<Int, VertexDataPoint<*>>>()
private var offset = 0
val length
get() = items.map { (_, v) -> v.component2().length }.sum()
class VertexDataRegister<T>(private val default: T, val provider: (default: T) -> VertexDataPoint<T>) {
operator fun provideDelegate(thisRef: VertexData, prop: KProperty<*>): VertexDataPoint<T> {
val delegate = provider(default)
thisRef.items[prop.name] = thisRef.offset to delegate
thisRef.offset += delegate.length
return delegate
}
}
fun int(value: Int = 0): VertexDataRegister<Int> = VertexDataRegister(value, ::SingleInt)
fun double(value: Double = 0.0): VertexDataRegister<Double> = VertexDataRegister(value, ::SingleDouble)
fun float(value: Float = 0f): VertexDataRegister<Float> = VertexDataRegister(value, ::SingleFloat)
fun bool(value: Boolean = false): VertexDataRegister<Boolean> = VertexDataRegister(value, ::SingleBoolean)
fun vec2(value: Vector2fc = Vector2f()): VertexDataRegister<Vector2fc> = VertexDataRegister(value, ::Vec2)
fun vec3(value: Vector3fc = Vector3f()): VertexDataRegister<Vector3fc> = VertexDataRegister(value, ::Vec3)
fun vec4(value: Vector4fc = Vector4f()): VertexDataRegister<Vector4fc> = VertexDataRegister(value, ::Vec4)
fun mat3(value: Matrix3fc = Matrix3f()): VertexDataRegister<Matrix3fc> = VertexDataRegister(value, ::Mat3)
fun mat4(value: Matrix4fc = Matrix4f()): VertexDataRegister<Matrix4fc> = VertexDataRegister(value, ::Mat4)
fun getDataPoint(prop: KProperty1<out VertexData, *>): VertexDataPoint<*>? = items[prop.name]?.component2()
fun getOffset(prop: KProperty1<out VertexData, *>): 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
}
}
}

@ -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<T>(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<Int>(GL_INT, value, 4) {
override fun write(position: Int, buffer: ByteBuffer) {
buffer.putInt(position, value)
}
}
class SingleFloat(value: Float) : VertexDataPoint<Float>(GL_FLOAT, value, 4) {
override fun write(position: Int, buffer: ByteBuffer) {
buffer.putFloat(position, value)
}
}
class SingleDouble(value: Double) : VertexDataPoint<Double>(GL_DOUBLE, value, 8) {
override fun write(position: Int, buffer: ByteBuffer) {
buffer.putDouble(position, value)
}
}
class SingleBoolean(value: Boolean) : VertexDataPoint<Boolean>(GL_BOOL, value, 1) {
override fun write(position: Int, buffer: ByteBuffer) {
buffer.put(position, if (value) 1 else 2)
}
}
class Vec2(value: Vector2fc) : VertexDataPoint<Vector2fc>(GL_FLOAT, value, 8, 2) {
override fun write(position: Int, buffer: ByteBuffer) {
value.get(position, buffer)
}
}
class Vec3(value: Vector3fc) : VertexDataPoint<Vector3fc>(GL_FLOAT, value, 12, 3) {
override fun write(position: Int, buffer: ByteBuffer) {
value.get(position, buffer)
}
}
class Vec4(value: Vector4fc) : VertexDataPoint<Vector4fc>(GL_FLOAT, value, 16, 4) {
override fun write(position: Int, buffer: ByteBuffer) {
value.get(position, buffer)
}
}
class Mat3(value: Matrix3fc) : VertexDataPoint<Matrix3fc>(GL_FLOAT, value, 3 * 3 * 4, 9) {
override fun write(position: Int, buffer: ByteBuffer) {
value.get(position, buffer)
}
}
class Mat4(value: Matrix4fc) : VertexDataPoint<Matrix4fc>(GL_FLOAT, value, 4 * 4 * 4, 16) {
override fun write(position: Int, buffer: ByteBuffer) {
value.get(position, buffer)
}
}
}

@ -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
}

@ -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
}

@ -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<DOMTreeUpdate.Remove> { (event) ->
removeNodeFromSearch(event.child)
@ -22,7 +22,9 @@ class Document : IDocument {
}
on<DOMTreeUpdate.Move> { (event) ->
updateKDTreeForNode(event.child)
if (event.child.shouldIndexLocation) {
updateKDTreeForNode(event.child)
}
}
on<NodeIDUpdate> { (event) ->
@ -36,19 +38,19 @@ class Document : IDocument {
}
on<NodeClassListUpdate.Added> { (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<NodeClassListUpdate.Removed> { (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<NodeModelUpdate> { (event) ->
updateKDTreeForNode(event.node)
}
on<NodeLocationIndexStateChange> { (event) ->
if (event.shouldIndex) {
kdTree.add(event.node)
} else {
kdTree.remove(event.node)
}
}
}
private val kdTree = KDTree(this)
private val allNodes: MutableMap<Long, INode<*>> = mutableMapOf()
private val byId: MutableMap<String, Long> = mutableMapOf()
private val byClass: MutableMap<String, MutableSet<Long>> = mutableMapOf()
private val byTag: MutableMap<String, MutableSet<Long>> = mutableMapOf()
private val byType: MutableMap<String, MutableSet<Long>> = mutableMapOf()
private val byRenderTarget: MutableMap<String, MutableSet<Long>> = 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<INode<*>> =
byClass[className]?.asSequence()?.mapNotNull(allNodes::get) ?: emptySequence()
override fun getNodesByTag(tagName: String): Sequence<INode<*>> =
byTag[tagName]?.asSequence()?.mapNotNull(allNodes::get) ?: emptySequence()
@Suppress("UNCHECKED_CAST")
override fun <T : INode<T>> getNodesByClass(className: String): Sequence<T> =
byType[className]?.asSequence()?.mapNotNull { nodeId ->
allNodes[nodeId]?.let {
(it.className == className) as? T
}
} ?: emptySequence()
@Suppress("UNCHECKED_CAST")
override fun <T : IRenderTarget<C>, C> getNodesByRenderTarget(targetType: String): Sequence<IRenderNode<*, T, C>> =
byRenderTarget[targetType]?.asSequence()?.mapNotNull(allNodes::get) as? Sequence<IRenderNode<*, T, C>>
?: 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 <reified T> on(topLevel: Boolean = false, noinline block: (Event<T>) -> Unit) = if (topLevel)

@ -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<INode<*>>
fun rebalance()
fun <C, T : IRenderTarget<C>, N : IRenderNode<*, T, C>> render(target: T, renderer: C) {
for (node in getNodesByRenderTarget<T, C>(target.type)) {
node.render(renderer)
}
}
}
inline fun <reified T : INode<T>> IDocument.createNode() = createNode(T::class)

@ -6,6 +6,9 @@ import org.joml.Matrix4dc
import java.util.concurrent.atomic.AtomicLong
interface INode<T : INode<T>> : Comparable<INode<*>>, INodeContainer {
val className: String
get() = this::class.java.name
/**
* The ID of this node
*/
@ -14,7 +17,7 @@ interface INode<T : INode<T>> : Comparable<INode<*>>, INodeContainer {
/**
* Set with all class names assigned to this node
*/
val classList: MutableSet<String>
val tagList: MutableSet<String>
/**
* Internal ID of this node, should be unique inside document
@ -31,6 +34,12 @@ interface INode<T : INode<T>> : Comparable<INode<*>>, 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<T : INode<T>> : Comparable<INode<*>>, 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.

@ -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<INode<*>>
fun getNodesByTag(tagName: String): Sequence<INode<*>>
/**
* Get all nodes inside this node with the Java class [className]
*/
fun <T : INode<T>> getNodesByClass(className: String): Sequence<T>
/**
* Get all nodes inside this node with the render target with type of [targetType]s
*/
fun <T : IRenderTarget<C>, C> getNodesByRenderTarget(targetType: String): Sequence<IRenderNode<*, T, C>>
/**
* find all nodes inside this node in region between [pointA] and [pointB]

@ -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<T : INode<T>>(document: IDocument?) : INode<T>, 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<T : INode<T>>(document: IDocument?) : INode<T>, 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<T : INode<T>>(document: IDocument?) : INode<T>, EventListene
override var absolute: Matrix4dc = Matrix4d()
protected set
override val classList: MutableSet<String> = ObservableSet {
override val tagList: MutableSet<String> = 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<T : INode<T>>(document: IDocument?) : INode<T>, 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<INode<*>> = nodes.toList()
@ -190,10 +195,16 @@ abstract class Node<T : INode<T>>(document: IDocument?) : INode<T>, EventListene
}
override fun getNodeById(id: String): INode<*>? = document?.getNodeById(id)?.takeIf { it.hasParent(this) }
override fun getNodesByClassName(className: String): Sequence<INode<*>> = if (nodes.isEmpty())
override fun getNodesByTag(tagName: String): Sequence<INode<*>> = if (nodes.isEmpty())
emptySequence()
else
document?.getNodesByClassName(className)?.filter { it.hasParent(this) } ?: emptySequence()
document?.getNodesByTag(tagName)?.filter { it.hasParent(this) } ?: emptySequence()
override fun <T : INode<T>> getNodesByClass(className: String): Sequence<T> =
document?.getNodesByClass<T>(className)?.filter { it.hasParent(this) } ?: emptySequence()
override fun <T : IRenderTarget<C>, C> getNodesByRenderTarget(targetType: String): Sequence<IRenderNode<*, T, C>> =
document?.getNodesByRenderTarget<T, C>(targetType)?.filter { it.hasParent(this) } ?: emptySequence()
override fun updateAbsolute() {
this.absolute = (this.parentNode?.absolute ?: Matrix4d()) * model

@ -1,7 +1,5 @@
package me.eater.threedom.dom
class PlainNode(document: IDocument?) : Node<PlainNode>(document) {
override fun cloneSelf(): PlainNode {
return PlainNode(document)
}
override fun cloneSelf(): PlainNode = PlainNode(document)
}

@ -7,8 +7,8 @@ sealed class NodeClassListUpdate {
abstract val node: INode<*>
@EventName("NodeClassesRemoved")
class Removed(val classNames: Set<String>, override val node: INode<*>) : NodeClassListUpdate()
class Removed(val tagNames: Set<String>, override val node: INode<*>) : NodeClassListUpdate()
@EventName("NodeClassesAdded")
class Added(val classNames: Set<String>, override val node: INode<*>) : NodeClassListUpdate()
class Added(val tagNames: Set<String>, override val node: INode<*>) : NodeClassListUpdate()
}

@ -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<*>)

@ -0,0 +1,10 @@
package me.eater.threedom.dom.render
import me.eater.threedom.dom.INode
interface IRenderNode<N : IRenderNode<N, T, C>, T : IRenderTarget<C>, C> :
INode<N> {
val target: T
fun render(renderer: C)
}

@ -0,0 +1,5 @@
package me.eater.threedom.dom.render
interface IRenderTarget<C> {
val type: String
}

@ -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 <T : Number> 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])

@ -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
}
})

@ -18,9 +18,11 @@ class PositionTest : StringSpec({
val doc: IDocument = Document()
val node = doc.createNode<PlainNode>()
doc.addNode(node)
node.shouldIndexLocation = true
node.model { setTranslation(10, 0, 10) }
node.absolute.translation shouldBe Vector3d(10, 0, 10)
val nodeTwo = doc.createNode<PlainNode>()
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<PlainNode>()
val nodeTwo = doc.createNode<PlainNode>()
val nodeThree = doc.createNode<PlainNode>()
listOf(node, nodeTwo, nodeThree).forEach { it.shouldIndexLocation = true }
doc.addNode(node)
node.addNode(nodeTwo)
nodeTwo.addNode(nodeThree)

Binary file not shown.
Loading…
Cancel
Save