diff --git a/README.md b/README.md index 0519ecba..ff7ab329 100644 --- a/README.md +++ b/README.md @@ -1 +1,38 @@ - \ No newline at end of file +## Mechanica, a 2D Game Engine in Kotlin +A powerful 2D Game Engine written in Kotlin. This project is still under development and it is not +thoroughly tested or documented but feel free to try it out nonetheless + +### Setting up the project with Gradle +Clone or download the repository and build with gradle + +Create a new Project with gradle. In the `settings.gradle` file add: + +```kotlin +includeBuild("C:/path/to/mechanica") +``` +And in the `build.gradle` file add a dependency on the project: +```kotlin +dependencies { + implementation("com.mechanica.engine:mechanica:1.0") +} +``` + +In the new project create a main method with something similar to the following: +```kotlin +fun main() { + Game.configure { + setViewport(height = 10.0) + } + + // Create an instance of the Drawer class, + // it can be used to draw anything from rounded rectangles to custom shaders + val draw = Drawer.create() + + // Start the game loop and draw text to the screen + Game.run { + draw.centered.grey.text("Hello, Mechanica") + } +} +``` + +More samples can be seen in the `samples` module in Mechanica diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/audio/AudioFile.kt b/application-interface/src/main/kotlin/com/mechanica/engine/audio/AudioFile.kt new file mode 100644 index 00000000..1ab8b1b3 --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/audio/AudioFile.kt @@ -0,0 +1,10 @@ +package com.mechanica.engine.audio + +import java.nio.ShortBuffer + +interface AudioFile { + val channels: Int + val sampleRate: Int + val buffer: ShortBuffer + val format: Int +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/audio/AudioObject.kt b/application-interface/src/main/kotlin/com/mechanica/engine/audio/AudioObject.kt new file mode 100644 index 00000000..d5ba48c2 --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/audio/AudioObject.kt @@ -0,0 +1,9 @@ +package com.mechanica.engine.audio + +import org.joml.Vector3f + +interface AudioObject { + var gain: Float + var position: Vector3f + var velocity: Vector3f +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/audio/Listener.kt b/application-interface/src/main/kotlin/com/mechanica/engine/audio/Listener.kt new file mode 100644 index 00000000..21dbe662 --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/audio/Listener.kt @@ -0,0 +1,24 @@ +package com.mechanica.engine.audio + +import com.mechanica.engine.context.loader.GLLoader +import org.joml.Vector3f + +interface Listener : AudioObject { + var distanceModel: DistanceModel + val orientation: Orientaion + + interface Orientaion { + var at: Vector3f + var up: Vector3f + } + + companion object : Listener by GLLoader.audioLoader.listener() + + enum class DistanceModel { + INVERSE, + LINEAR, + EXPONENTIAL; + + var clamped: Boolean = false + } +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/audio/Sound.kt b/application-interface/src/main/kotlin/com/mechanica/engine/audio/Sound.kt new file mode 100644 index 00000000..f44acd54 --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/audio/Sound.kt @@ -0,0 +1,7 @@ +package com.mechanica.engine.audio + +interface Sound : AudioFile { + val id: Int + val frequency: Int + fun destroy() +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/audio/SoundSource.kt b/application-interface/src/main/kotlin/com/mechanica/engine/audio/SoundSource.kt new file mode 100644 index 00000000..9ed375a3 --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/audio/SoundSource.kt @@ -0,0 +1,21 @@ +package com.mechanica.engine.audio + +import com.mechanica.engine.context.loader.GLLoader +import com.mechanica.engine.resources.AudioResource + +interface SoundSource : AudioObject { + val id: Int + val sound: Sound + var pitch: Float + var rolloff: Float + var maxDistance: Float + var referenceDistance: Float + fun play() + fun pause() + fun stop() + fun destroy() + + companion object { + fun create(res: AudioResource) = GLLoader.audioLoader.source(res.sound) + } +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/configuration/Configurable.kt b/application-interface/src/main/kotlin/com/mechanica/engine/configuration/Configurable.kt new file mode 100644 index 00000000..4625d7be --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/configuration/Configurable.kt @@ -0,0 +1,7 @@ +package com.mechanica.engine.configuration + +import com.mechanica.engine.context.Application + +interface Configurable { + fun configureAs(application: Application, setup: T.() -> Unit) +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/context/Application.kt b/application-interface/src/main/kotlin/com/mechanica/engine/context/Application.kt new file mode 100644 index 00000000..4bafd922 --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/context/Application.kt @@ -0,0 +1,13 @@ +package com.mechanica.engine.context + +import com.mechanica.engine.display.Window + +interface Application { + + fun initialize(window: Window) + + fun terminate() + + fun startFrame() + +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/context/GLInitializer.kt b/application-interface/src/main/kotlin/com/mechanica/engine/context/GLInitializer.kt index c67dd1ed..5de13231 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/context/GLInitializer.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/context/GLInitializer.kt @@ -1,5 +1,7 @@ package com.mechanica.engine.context +import com.mechanica.engine.context.callbacks.EventCallbacks +import com.mechanica.engine.context.loader.DisplayLoader import com.mechanica.engine.context.loader.GLLoader object GLInitializer { @@ -7,9 +9,20 @@ object GLInitializer { internal val loader: GLLoader get() = _loader ?: throw UninitializedPropertyAccessException("The OpenGL context has not been initialized") - fun initialize(loader: GLLoader) { + private var _displayLoader: DisplayLoader? = null + internal val displayLoader: DisplayLoader + get() = _displayLoader ?: throw UninitializedPropertyAccessException("The OpenGL context has not been initialized") + + fun initializeDisplay(loader: DisplayLoader) { + if (_loader == null) { + _displayLoader = loader + } else throw IllegalStateException("The Display context has already been initialized") + } + + fun initialize(loader: GLLoader): EventCallbacks { if (_loader == null) { _loader = loader + return EventCallbacks.create() } else throw IllegalStateException("The OpenGl context has already been initialized") } } \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/context/callbacks/EventCallbacks.kt b/application-interface/src/main/kotlin/com/mechanica/engine/context/callbacks/EventCallbacks.kt new file mode 100644 index 00000000..03528e88 --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/context/callbacks/EventCallbacks.kt @@ -0,0 +1,20 @@ +package com.mechanica.engine.context.callbacks + +import com.mechanica.engine.input.TextInput + +interface EventCallbacks { + val keyboardHandler: KeyboardHandler + val mouseHandler: MouseHandler + + companion object { + fun prepare() { + TextInput.prepare() + MouseHandler.prepare() + } + + internal fun create(): EventCallbacks = object : EventCallbacks { + override val keyboardHandler = KeyboardHandler.create() + override val mouseHandler = MouseHandler.create() + } + } +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/context/callbacks/KeyboardHandler.kt b/application-interface/src/main/kotlin/com/mechanica/engine/context/callbacks/KeyboardHandler.kt new file mode 100644 index 00000000..2717c0c2 --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/context/callbacks/KeyboardHandler.kt @@ -0,0 +1,18 @@ +package com.mechanica.engine.context.callbacks + +import com.mechanica.engine.input.KeyInput +import com.mechanica.engine.input.TextInput + +interface KeyboardHandler { + fun keyPressed(key: Int) + fun keyReleased(key: Int) + fun textInput(codepoint: Int) + + companion object { + internal fun create(): KeyboardHandler = object : KeyboardHandler { + override fun keyPressed(key: Int) = KeyInput.addPressed(key) + override fun keyReleased(key: Int) = KeyInput.removePressed(key) + override fun textInput(codepoint: Int) = TextInput.addCodePoint(codepoint) + } + } +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/context/callbacks/MouseHandler.kt b/application-interface/src/main/kotlin/com/mechanica/engine/context/callbacks/MouseHandler.kt new file mode 100644 index 00000000..59cd127d --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/context/callbacks/MouseHandler.kt @@ -0,0 +1,57 @@ +package com.mechanica.engine.context.callbacks + +import com.mechanica.engine.input.KeyInput + +interface MouseHandler { + fun buttonPressed(key: Int) + fun buttonReleased(key: Int) + fun cursorMoved(x: Double, y: Double) + fun scroll(x: Double, y: Double) + + companion object { + var cursorX: Double = 0.0 + private set + var cursorY: Double = 0.0 + private set + + var scrollX: Double = 0.0 + private set + var scrollY: Double = 0.0 + private set + + internal fun prepare() { + KeyInput.removePressed(1000) + KeyInput.removePressed(1001) + KeyInput.removePressed(1002) + scrollX = 0.0 + scrollY = 0.0 + } + + internal fun create() = object : MouseHandler { + override fun buttonPressed(key: Int) { + KeyInput.addPressed(key) + } + + override fun buttonReleased(key: Int) { + KeyInput.removePressed(key) + } + + override fun cursorMoved(x: Double, y: Double) { + cursorX = x + cursorY = y + } + + override fun scroll(x: Double, y: Double) { + if (y > 0.0) { + KeyInput.addPressed(1000) + } else if (y < 0.0) { + KeyInput.addPressed(1001) + } + KeyInput.addPressed(1002) + scrollX = x + scrollY = y + } + + } + } +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/AudioLoader.kt b/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/AudioLoader.kt new file mode 100644 index 00000000..57ee71c6 --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/AudioLoader.kt @@ -0,0 +1,11 @@ +package com.mechanica.engine.context.loader + +import com.mechanica.engine.audio.Listener +import com.mechanica.engine.audio.Sound +import com.mechanica.engine.audio.SoundSource + +interface AudioLoader { + fun sound(file: String): Sound + fun source(sound: Sound): SoundSource + fun listener(): Listener +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/BufferLoader.kt b/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/BufferLoader.kt index b2deb82a..8b10824b 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/BufferLoader.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/BufferLoader.kt @@ -1,5 +1,6 @@ package com.mechanica.engine.context.loader +import com.mechanica.engine.audio.AudioFile import java.nio.ByteBuffer import java.nio.FloatBuffer import java.nio.IntBuffer diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/DisplayLoader.kt b/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/DisplayLoader.kt new file mode 100644 index 00000000..37ddd905 --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/DisplayLoader.kt @@ -0,0 +1,17 @@ +package com.mechanica.engine.context.loader + +import com.mechanica.engine.context.GLInitializer +import com.mechanica.engine.display.Monitor +import com.mechanica.engine.display.Window + +interface DisplayLoader { + fun createWindow(title: String, width: Int, height: Int): Window + fun createWindow(title: String, monitor: Monitor): Window + fun createWindow(title: String, width: Int, height: Int, monitor: Monitor): Window + + val allMonitors: Array + + fun getPrimaryMonitor(): Monitor + + companion object : DisplayLoader by GLInitializer.displayLoader +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/FontLoader.kt b/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/FontLoader.kt index bfdd2f76..a9380c8e 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/FontLoader.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/FontLoader.kt @@ -2,9 +2,10 @@ package com.mechanica.engine.context.loader import com.mechanica.engine.text.Font import com.mechanica.engine.resources.Resource +import com.mechanica.engine.text.FontAtlasConfiguration interface FontLoader { val defaultFont: Font - fun font(res: Resource): Font + fun font(res: Resource, initializer: FontAtlasConfiguration.() -> Unit): Font } \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/GLLoader.kt b/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/GLLoader.kt index a4089731..480ddfe0 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/GLLoader.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/GLLoader.kt @@ -3,6 +3,8 @@ package com.mechanica.engine.context.loader import com.mechanica.engine.context.GLInitializer import com.mechanica.engine.shader.qualifiers.AttributeQualifier import com.mechanica.engine.shader.qualifiers.Qualifier +import com.mechanica.engine.shader.script.Shader +import com.mechanica.engine.shader.script.ShaderScript import com.mechanica.engine.vertices.ElementArrayType interface GLLoader { @@ -11,10 +13,17 @@ interface GLLoader { val bufferLoader: BufferLoader val fontLoader: FontLoader val graphicsLoader: GraphicsLoader + val audioLoader: AudioLoader + val inputLoader: InputLoader fun createAttributeLoader(qualifier: AttributeQualifier): AttributeLoader fun createUniformLoader(qualifier: Qualifier): UniformLoader fun createElementArray(): ElementArrayType + fun defaultShader( + vertex: ShaderScript, + fragment: ShaderScript, + tessellation: ShaderScript?, + geometry: ShaderScript?): Shader companion object : GLLoader by GLInitializer.loader } \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/InputLoader.kt b/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/InputLoader.kt new file mode 100644 index 00000000..04e44b1d --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/InputLoader.kt @@ -0,0 +1,7 @@ +package com.mechanica.engine.context.loader + +import com.mechanica.engine.input.KeyIDs + +interface InputLoader { + fun keyIds(): KeyIDs +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/UniformLoader.kt b/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/UniformLoader.kt index b197b05c..dcf7d5bf 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/UniformLoader.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/context/loader/UniformLoader.kt @@ -1,7 +1,7 @@ package com.mechanica.engine.context.loader import com.mechanica.engine.shader.qualifiers.Qualifier -import com.mechanica.engine.shader.vars.uniforms.vars.* +import com.mechanica.engine.shader.uniforms.vars.* import org.joml.Matrix4f interface UniformLoader { diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/display/Monitor.kt b/application-interface/src/main/kotlin/com/mechanica/engine/display/Monitor.kt new file mode 100644 index 00000000..6f9114b9 --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/display/Monitor.kt @@ -0,0 +1,27 @@ +package com.mechanica.engine.display + +import com.mechanica.engine.context.loader.DisplayLoader +import com.mechanica.engine.context.loader.GLLoader + + +interface Monitor { + val id: Long + val name: String + val size: Size + val contentScale: ContentScale + + val width: Int + val height: Int + + data class Size(val width_mm: Int, val height_mm: Int) + data class ContentScale(val xScale: Float, val yScale: Float) + + companion object { + val allMonitors: Array + get() = DisplayLoader.allMonitors + + fun getPrimaryMonitor(): Monitor { + return DisplayLoader.getPrimaryMonitor() + } + } +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/display/Window.kt b/application-interface/src/main/kotlin/com/mechanica/engine/display/Window.kt new file mode 100644 index 00000000..c370ed12 --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/display/Window.kt @@ -0,0 +1,66 @@ +package com.mechanica.engine.display + +import com.mechanica.engine.context.loader.DisplayLoader +import com.mechanica.engine.resources.Resource +import java.nio.ByteBuffer + +interface Window { + val title: String + val id: Long + val width: Int + val height: Int + val aspectRatio: Double + var isFocused: Boolean + var isIconified: Boolean + var isMaximized: Boolean + val isHovered: Boolean + var isVisible: Boolean + var isResizable: Boolean + var isDecorated: Boolean + var isFloating: Boolean + var vSync: Boolean + var opacity: Float + val position: Position + val resolution: Dimension + val size: Dimension + val isResizing: Boolean + var shouldClose: Boolean + val isFullscreen: Boolean + + fun addRefreshCallback(callback: (Window) -> Unit) + fun requestAttention() + fun setIcon(resource: Resource) + fun setIcon(width: Int, height: Int, imageBuffer: ByteBuffer) + fun setFullscreen() + fun setFullscreen(monitor: Monitor) + fun setFullscreen(monitor: Monitor, width: Int, height: Int, refreshRate: Int = 60) + fun exitFullscreen(width: Int = -1, height: Int = -1) + fun destroy() + fun update(): Boolean + + interface Dimension { + val width: Int + val height: Int + } + + interface Position { + var x: Int + var y: Int + + fun set(x: Int, y: Int) + } + + companion object { + fun create(title: String, width: Int, height: Int): Window { + return DisplayLoader.createWindow(title, width, height) + } + + fun create(title: String, monitor: Monitor): Window { + return DisplayLoader.createWindow(title, monitor) + } + + fun create(title: String, width: Int, height: Int, monitor: Monitor): Window { + return DisplayLoader.createWindow(title, width, height, monitor) + } + } +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/input/KeyID.kt b/application-interface/src/main/kotlin/com/mechanica/engine/input/KeyID.kt new file mode 100644 index 00000000..fd7fb00e --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/input/KeyID.kt @@ -0,0 +1,6 @@ +package com.mechanica.engine.input + +interface KeyID { + val id: Int + val label: String +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/input/KeyIDs.kt b/application-interface/src/main/kotlin/com/mechanica/engine/input/KeyIDs.kt new file mode 100644 index 00000000..8e05518b --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/input/KeyIDs.kt @@ -0,0 +1,141 @@ +package com.mechanica.engine.input + +import com.mechanica.engine.context.loader.GLLoader + +interface KeyIDs { + val UNKNOWN: KeyID + val M1: KeyID + val M2: KeyID + val M3: KeyID + val M4: KeyID + val M5: KeyID + val M6: KeyID + val M7: KeyID + val M8: KeyID + val SPACE: KeyID + val APOSTROPHE: KeyID + val COMMA: KeyID + val MINUS: KeyID + val PERIOD: KeyID + val SLASH: KeyID + val N0: KeyID + val N1: KeyID + val N2: KeyID + val N3: KeyID + val N4: KeyID + val N5: KeyID + val N6: KeyID + val N7: KeyID + val N8: KeyID + val N9: KeyID + val SEMICOLON: KeyID + val EQUAL: KeyID + val A: KeyID + val B: KeyID + val C: KeyID + val D: KeyID + val E: KeyID + val F: KeyID + val G: KeyID + val H: KeyID + val I: KeyID + val J: KeyID + val K: KeyID + val L: KeyID + val M: KeyID + val N: KeyID + val O: KeyID + val P: KeyID + val Q: KeyID + val R: KeyID + val S: KeyID + val T: KeyID + val U: KeyID + val V: KeyID + val W: KeyID + val X: KeyID + val Y: KeyID + val Z: KeyID + val LEFT_BRACKET: KeyID + val BACKSLASH: KeyID + val RIGHT_BRACKET: KeyID + val GRAVE_ACCENT: KeyID + val WORLD_1: KeyID + val WORLD_2: KeyID + val ESC: KeyID + val ENTER: KeyID + val TAB: KeyID + val BACKSPACE: KeyID + val INSERT: KeyID + val DELETE: KeyID + val RIGHT: KeyID + val LEFT: KeyID + val DOWN: KeyID + val UP: KeyID + val PAGE_UP: KeyID + val PAGE_DOWN: KeyID + val HOME: KeyID + val END: KeyID + val CAPS_LOCK: KeyID + val SCROLL_LOCK: KeyID + val NUM_LOCK: KeyID + val PRINT_SCREEN: KeyID + val PAUSE: KeyID + val F1: KeyID + val F2: KeyID + val F3: KeyID + val F4: KeyID + val F5: KeyID + val F6: KeyID + val F7: KeyID + val F8: KeyID + val F9: KeyID + val F10: KeyID + val F11: KeyID + val F12: KeyID + val F13: KeyID + val F14: KeyID + val F15: KeyID + val F16: KeyID + val F17: KeyID + val F18: KeyID + val F19: KeyID + val F20: KeyID + val F21: KeyID + val F22: KeyID + val F23: KeyID + val F24: KeyID + val F25: KeyID + val KP_0: KeyID + val KP_1: KeyID + val KP_2: KeyID + val KP_3: KeyID + val KP_4: KeyID + val KP_5: KeyID + val KP_6: KeyID + val KP_7: KeyID + val KP_8: KeyID + val KP_9: KeyID + val KP_DECIMAL: KeyID + val KP_DIVIDE: KeyID + val KP_MULTIPLY: KeyID + val KP_SUBTRACT: KeyID + val KP_ADD: KeyID + val KP_ENTER: KeyID + val KP_EQUAL: KeyID + val LSHIFT: KeyID + val LCTRL: KeyID + val LALT: KeyID + val LEFT_SUPER: KeyID + val RSHIFT: KeyID + val RCTRL: KeyID + val RALT: KeyID + val RIGHT_SUPER: KeyID + val MENU: KeyID + + val SCROLL_UP: KeyID + val SCROLL_DOWN: KeyID + val SCROLL: KeyID + + companion object : KeyIDs by GLLoader.inputLoader.keyIds() +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/input/KeyInput.kt b/application-interface/src/main/kotlin/com/mechanica/engine/input/KeyInput.kt new file mode 100644 index 00000000..506b1acc --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/input/KeyInput.kt @@ -0,0 +1,27 @@ +package com.mechanica.engine.input + +object KeyInput { + + var pressedKeyCount: Int = 0 + private set + val any: Boolean + get() = pressedKeyCount > 0 + private val pressedKeys = BooleanArray(1500) + + fun isPressed(keyId: Int) = pressedKeys[keyId] + + internal fun addPressed(key: Int) { + pressedKeyCount++ + if (key >= 0) { + pressedKeys[key] = true + } + } + + internal fun removePressed(key: Int) { + if (pressedKeyCount > 0) pressedKeyCount-- + if (key >= 0) { + pressedKeys[key] = false + } + } + +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/input/TextInput.kt b/application-interface/src/main/kotlin/com/mechanica/engine/input/TextInput.kt new file mode 100644 index 00000000..c51e40d9 --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/input/TextInput.kt @@ -0,0 +1,41 @@ +package com.mechanica.engine.input + +import com.mechanica.engine.context.callbacks.KeyboardHandler +import com.mechanica.engine.util.NullTerminatedChars + +object TextInput { + + @PublishedApi + internal val codepoints = NullTerminatedChars(10) + var textInputCount: Int = 0 + private set + val hasTextInput: Boolean + get() = textInputCount != 0 + + internal fun prepare() { + codepoints.clear() + textInputCount = 0 + } + + fun getCodepoints(sb: StringBuilder, offset: Int = -1): Int { + return codepoints.get(sb, offset) + } + + fun getCodepoints(charArray: CharArray) { + codepoints.get(charArray) + } + + fun getCodepoints(chars: NullTerminatedChars) { + codepoints.get(chars) + } + + internal fun addCodePoint(cp: Int) { + codepoints.addCodePoint(cp) + textInputCount++ + } + + inline fun forEachChar(operation: (Char) -> Unit) { + codepoints.forEach(operation) + } + +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/models/ImageModel.kt b/application-interface/src/main/kotlin/com/mechanica/engine/models/ImageModel.kt index 46c86093..3afdd947 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/models/ImageModel.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/models/ImageModel.kt @@ -1,8 +1,16 @@ package com.mechanica.engine.models +import com.mechanica.engine.shader.qualifiers.Attribute + class ImageModel(image: Image, vararg inputs: Bindable) : Model(image, *inputs) { + constructor(image: Image) + : this(image, + Attribute.location(0).vec3().createUnitQuad(), + Attribute.location(1).vec2().createInvertedUnitQuad() + ) + var image: Image get() = inputs[0] as Image set(value) { diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/models/Model.kt b/application-interface/src/main/kotlin/com/mechanica/engine/models/Model.kt index d73270de..5243239b 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/models/Model.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/models/Model.kt @@ -24,7 +24,11 @@ open class Model(vararg inputs: Bindable, fun bind() { for (vbo in inputs) { - vbo.bind() + if (vbo is VertexBuffer<*>) { + vbo.safeBind() + } else { + vbo.bind() + } } } diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/models/TextModel.kt b/application-interface/src/main/kotlin/com/mechanica/engine/models/TextModel.kt index a935ab12..afc6bd85 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/models/TextModel.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/models/TextModel.kt @@ -8,13 +8,14 @@ import com.mechanica.engine.utils.createIndicesArrayForQuads import com.mechanica.engine.vertices.AttributeArray import com.mechanica.engine.vertices.IndexArray import com.mechanica.engine.vertices.FloatBufferMaker +import kotlin.math.max class TextModel(text: Text, positionBufferMaker: FloatBufferMaker = Attribute(0).vec3(), texCoordsBufferMaker: FloatBufferMaker = Attribute(1).vec2()) : Model( positionBufferMaker.createBuffer(text.positions), texCoordsBufferMaker.createBuffer(text.texCoords), - IndexArray.create(*createIndicesArrayForQuads(20)), + IndexArray.create(*createIndicesArrayForQuads(max(text.positions.size/2, 20))), Image.invoke(text.font.atlas.id), draw = { model -> GLLoader.graphicsLoader.drawElements(model) @@ -24,13 +25,23 @@ class TextModel(text: Text, private val texCoordsAttribute = inputs[1] as AttributeArray private val indexArray = inputs[2] as IndexArray - private var textHolder: Text = text + private var atlas + get() = inputs[3] as Image + set(value) { + inputs[3] = value + } - var text: String = textHolder.text + var textHolder: Text = text set(value) { + atlas = value.font.atlas field = value - textHolder.text = value - updateTextHolder() + } + + var string: String + get() = textHolder.string + set(value) { + textHolder.string = value + updateTextHolder(textHolder) } val lineCount: Int @@ -44,20 +55,17 @@ class TextModel(text: Text, } fun setText(text: Text) { - if (text.text != this.text || textHolder != text) { - this.textHolder = text - this.text = textHolder.text - updateTextHolder() - } + this.textHolder = text + updateTextHolder(text) } - private fun updateTextHolder() { - positionAttribute.set(textHolder.positions, 0, textHolder.vertexCount) - texCoordsAttribute.set(textHolder.texCoords, 0, textHolder.vertexCount) - vertexCount = textHolder.vertexCount + private fun updateTextHolder(text: Text) { + positionAttribute.set(text.positions, 0, text.vertexCount) + texCoordsAttribute.set(text.texCoords, 0, text.vertexCount) + vertexCount = text.vertexCount if (vertexCount > indexArray.vertexCount) { - indexArray.set(createIndicesArrayForQuads(vertexCount*2/6)) + indexArray.set(createIndicesArrayForQuads(vertexCount)) } } diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/persistence/PersistenceMap.kt b/application-interface/src/main/kotlin/com/mechanica/engine/persistence/PersistenceMap.kt new file mode 100644 index 00000000..6c8fe36c --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/persistence/PersistenceMap.kt @@ -0,0 +1,68 @@ +package com.mechanica.engine.persistence + +import com.mechanica.engine.resources.ExternalResource +import com.mechanica.engine.util.StringMapSerializer +import kotlinx.serialization.UnstableDefault +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonConfiguration +import java.io.FileNotFoundException + +class PersistenceMap private constructor(private val path: String, private val map: HashMap) : Map by map { + constructor(path: String) : this(path, HashMap()) + + private val serializer = StringMapSerializer() + + fun store() { + val json = Json(JsonConfiguration.Stable) + val string = json.stringify(serializer, map) + val file = ExternalResource(path, true) + file.write(string) + } + + @OptIn(UnstableDefault::class) + fun populate() { + val string = try { + val file = ExternalResource(path, false) + file.contents + } catch (ex: FileNotFoundException) { + "{}" + } + + val map = Json.parse(serializer, string) + map.forEach { + this.map[it.key] = it.value + } + } + + fun put(name: String, value: Any) { + map[name] = value + } + + fun getDouble(name: String, default: Double = 0.0): Double { + return map[name] as? Double ?: default + } + + fun getFloat(name: String, default: Float = 0f): Float { + return map[name] as? Float ?: default + } + + fun getBoolean(name: String, default: Boolean = false): Boolean { + return map[name] as? Boolean ?: default + } + + fun getInt(name: String, default: Int = 0): Int { + return map[name] as? Int ?: default + } + + fun getLong(name: String, default: Long = 0): Long { + return map[name] as? Long ?: default + } + + fun getString(name: String, default: String = ""): String { + return map[name] as? String ?: default + } + + override fun toString() = map.toString() + +} + diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/persistence/PersistenceUtil.kt b/application-interface/src/main/kotlin/com/mechanica/engine/persistence/PersistenceUtil.kt new file mode 100644 index 00000000..84768d23 --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/persistence/PersistenceUtil.kt @@ -0,0 +1,21 @@ +package com.mechanica.engine.persistence + +private val map = PersistenceMap("res/data/persistence.json") + +fun persistent(default: Boolean, instance: String? = null) = PersistentVariable(map, default, instance) + +fun persistent(default: Int, instance: String? = null) = PersistentVariable(map, default, instance) +fun persistent(default: Long, instance: String? = null) = PersistentVariable(map, default, instance) + +fun persistent(default: Float, instance: String? = null) = PersistentVariable(map, default, instance) +fun persistent(default: Double, instance: String? = null) = PersistentVariable(map, default, instance) + +fun persistent(default: String, instance: String? = null) = PersistentVariable(map, default, instance) + +fun storeData() { + map.store() +} + +fun populateData() { + map.populate() +} diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/persistence/PersistentVariable.kt b/application-interface/src/main/kotlin/com/mechanica/engine/persistence/PersistentVariable.kt new file mode 100644 index 00000000..4945b21f --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/persistence/PersistentVariable.kt @@ -0,0 +1,64 @@ +package com.mechanica.engine.persistence + +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +class PersistentVariable( + private val map: PersistenceMap, + private val defaultValue: T, + private val instance: String? = null) : ReadWriteProperty { + + private var hasRetrieved = false + private var value: T = defaultValue + + private var fullPropertyName: String? = null + + override fun getValue(thisRef: Any, property: KProperty<*>): T { + if (!hasRetrieved) { + val name = fullPropertyName(thisRef, property) + value = if (instance == null) { + map.retrieveSingleValue(name) + } else { + map.retrieveInstancedValue(instance, name) + } + hasRetrieved = true + } + return value + } + + private fun Map<*, *>.retrieveSingleValue(name: String): T { + @Suppress("UNCHECKED_CAST") + return this[name] as? T ?: defaultValue + } + + private fun Map<*, *>.retrieveInstancedValue(instance: String, name: String): T { + val subMap = subMap(name) ?: return defaultValue + + return subMap.retrieveSingleValue(instance) + } + + private fun Map<*, *>.subMap(name: String) : HashMap? { + @Suppress("UNCHECKED_CAST") + return this[name] as? HashMap + } + + override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { + this.value = value + val name = fullPropertyName(thisRef, property) + if (instance == null) { + map.put(name, value) + } else { + map.subMap(name)?.put(instance, value) ?: map.put(name, HashMap().also { it[instance] = value }) + } + } + + private fun fullPropertyName(ref: Any, property: KProperty<*>): String { + val fullName = this.fullPropertyName + return if (fullName != null) fullName + else { + val newName = ref::class.java.canonicalName + "." + property.name + fullPropertyName = newName + newName + } + } +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/resources/AudioResource.kt b/application-interface/src/main/kotlin/com/mechanica/engine/resources/AudioResource.kt new file mode 100644 index 00000000..05e14bdf --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/resources/AudioResource.kt @@ -0,0 +1,18 @@ +package com.mechanica.engine.resources + +import com.mechanica.engine.audio.AudioFile +import com.mechanica.engine.audio.Sound +import com.mechanica.engine.audio.SoundSource +import com.mechanica.engine.context.loader.GLLoader +import java.nio.ShortBuffer + +class AudioResource(res: Resource, val sound: Sound) : GenericResource by res, AudioFile by sound { + + constructor(res: Resource) : this(res, GLLoader.audioLoader.sound(res.path)) + constructor(file: String) : this(Resource(file)) + + override val buffer: ShortBuffer + get() = sound.buffer + + fun createSource(): SoundSource = GLLoader.audioLoader.source(sound) +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/resources/ExternalResource.kt b/application-interface/src/main/kotlin/com/mechanica/engine/resources/ExternalResource.kt index 1f52d9e9..f488830d 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/resources/ExternalResource.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/resources/ExternalResource.kt @@ -68,7 +68,7 @@ class ExternalResource(filePath: String, createIfAbsent: Boolean = false) : Reso return if (createIfAbsent) { try { Resource(filePath) - } catch (ex: java.lang.IllegalStateException) { + } catch (ex: FileNotFoundException) { val newFile = File("${getResourceLocation()}/$filePath") newFile.parentFile.mkdirs() newFile.createNewFile() diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/resources/GenericResource.kt b/application-interface/src/main/kotlin/com/mechanica/engine/resources/GenericResource.kt new file mode 100644 index 00000000..69e6cb53 --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/resources/GenericResource.kt @@ -0,0 +1,12 @@ +package com.mechanica.engine.resources + +import java.io.InputStream +import java.nio.Buffer + +interface GenericResource { + val path: String + val stream: InputStream + val lines: List + val contents: String + val buffer: Buffer +} \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/resources/Resource.kt b/application-interface/src/main/kotlin/com/mechanica/engine/resources/Resource.kt index bf89fb6a..872e79dc 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/resources/Resource.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/resources/Resource.kt @@ -2,24 +2,24 @@ package com.mechanica.engine.resources import com.mechanica.engine.context.loader.GLLoader import java.io.BufferedReader +import java.io.FileNotFoundException import java.io.InputStream import java.io.InputStreamReader import java.net.URI import java.net.URL +import java.nio.Buffer import java.nio.ByteBuffer import kotlin.streams.toList -interface Resource { - val path: String - val stream: InputStream - val lines: List +interface Resource : GenericResource { + override val lines: List get() { val reader = BufferedReader(InputStreamReader(stream)) val lines = reader.lines().toList() reader.close() return lines } - val contents: String + override val contents: String get() { val sb = StringBuilder() lines.forEach { @@ -27,7 +27,7 @@ interface Resource { } return sb.toString() } - val buffer: ByteBuffer + override val buffer: ByteBuffer get() { val bytes = stream.readAllBytes() val buffer = GLLoader.bufferLoader.byteBuffer(bytes.size) @@ -40,7 +40,7 @@ interface Resource { operator fun invoke(file: String): Resource { val fileForURL = file.replace("\\", "/") - val url = getResourceURL(fileForURL) ?: throw IllegalStateException("Resource not found at $fileForURL") + val url = getResourceURL(fileForURL) ?: throw FileNotFoundException("Resource not found at $fileForURL") return ResourceImpl(url) } diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/resources/ResourceDirectory.kt b/application-interface/src/main/kotlin/com/mechanica/engine/resources/ResourceDirectory.kt index 2ef84494..43a2a7cf 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/resources/ResourceDirectory.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/resources/ResourceDirectory.kt @@ -1,41 +1,73 @@ package com.mechanica.engine.resources +import java.io.FileNotFoundException import java.net.URI import java.nio.file.* import java.util.stream.Stream +class ResourceDirectory(directory: String, recursive: Boolean = false) { + val resources: Array + val folders: Array -class ResourceDirectory(directory: String): Iterable { - private val resources: Array val fileCount: Int get() = resources.size + val folderCount: Int + get() = folders.size + + val name: String + init { - val list = ArrayList() + val resourceList = ArrayList() + val directoryList = ArrayList() + + val pathString = directory.removeSuffix("\\").removeSuffix("/") + "\\" - val uri: URI = this::class.java.getResource(directory).toURI() + val uri: URI = this::class.java.getResource(pathString)?.toURI() ?: throw FileNotFoundException("No file or directory found at $directory") var fileSystem: FileSystem? = null val path = if (uri.scheme == "jar") { fileSystem = FileSystems.newFileSystem(uri, emptyMap()) - fileSystem.getPath(directory) + fileSystem.getPath(pathString) } else { Paths.get(uri) } + name = path.fileName.toString() + val walk: Stream = Files.walk(path, 1) for (e in walk) { - if (!Files.isDirectory(e)) - list.add(Resource(e.toUri())) + if (!Files.isDirectory(e)) { + resourceList.add(Resource(e.toUri())) + } else if (Files.isDirectory(e)) { + val relative = path.relativize(e).toString() + if (relative.isNotEmpty() && relative != "." && relative != "..") { + directoryList.add(ResourceDirectory(pathString + relative, recursive)) + } + } } fileSystem?.close() - list.sortBy { it.path } - resources = list.toTypedArray() + resourceList.sortBy { it.path } + resources = resourceList.toTypedArray() + + folders = directoryList.toTypedArray() } - override fun iterator() = resources.iterator() + fun getFile(index: Int) = resources[index] + fun getFolder(index: Int) = folders[index] + inline fun forEachFile(operation: (Resource) -> Unit) { + for (i in 0 until fileCount) { + operation(getFile(i)) + } + } + + inline fun forEachFolder(operation: (ResourceDirectory) -> Unit) { + for (i in 0 until folderCount) { + operation(getFolder(i)) + } + } } \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/shader/qualifiers/Attribute.kt b/application-interface/src/main/kotlin/com/mechanica/engine/shader/qualifiers/Attribute.kt index 93d273c3..381efb7e 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/shader/qualifiers/Attribute.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/shader/qualifiers/Attribute.kt @@ -13,7 +13,7 @@ import com.mechanica.engine.vertices.FloatBufferMaker import org.joml.Vector3f import org.joml.Vector4f -class Attribute( +class Attribute internal constructor( override val location: Int, private val variables: ScriptVariables? = null) : AttributeQualifier, AttributeVars { @@ -81,8 +81,19 @@ class Attribute( } companion object { - operator fun invoke(location: Int): AttributeVars { - return Attribute(location) + private val attributeLocations = Array(20) { null} + + fun location(location: Int): AttributeVars { + return if (location > attributeLocations.lastIndex) { + Attribute(location) + } else { + val nullable = attributeLocations[location] + if (nullable == null) { + val attribute = Attribute(location) + attributeLocations[location] = attribute + attribute + } else nullable + } } } } \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/shader/qualifiers/Uniform.kt b/application-interface/src/main/kotlin/com/mechanica/engine/shader/qualifiers/Uniform.kt index d2712a4a..36bd9a4c 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/shader/qualifiers/Uniform.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/shader/qualifiers/Uniform.kt @@ -3,11 +3,12 @@ package com.mechanica.engine.shader.qualifiers import com.mechanica.engine.context.loader.GLLoader import com.mechanica.engine.context.loader.UniformLoader import com.mechanica.engine.shader.script.ScriptVariables +import com.mechanica.engine.shader.uniforms.UniformVars +import com.mechanica.engine.shader.uniforms.vars.* import com.mechanica.engine.shader.vars.ShaderType import com.mechanica.engine.shader.vars.ShaderVariable -import com.mechanica.engine.shader.vars.uniforms.* -import com.mechanica.engine.shader.vars.uniforms.vars.* import org.joml.Matrix4f +import kotlin.reflect.KProperty class Uniform(private val variables: ScriptVariables) : Qualifier, UniformVars { override val qualifierName: String = "uniform" @@ -21,6 +22,9 @@ class Uniform(private val variables: ScriptVariables) : Qualifier, UniformVars { override val qualifier = this@Uniform override val type = ShaderType.get(type) override fun loadUniform() { load(value) } + override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { + this.value = value + } } return addVariable(v) } diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/shader/script/ScriptVariables.kt b/application-interface/src/main/kotlin/com/mechanica/engine/shader/script/ScriptVariables.kt index afd251f5..2454290e 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/shader/script/ScriptVariables.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/shader/script/ScriptVariables.kt @@ -1,7 +1,7 @@ package com.mechanica.engine.shader.script import com.mechanica.engine.shader.vars.ShaderVariable -import com.mechanica.engine.shader.vars.uniforms.vars.UniformVar +import com.mechanica.engine.shader.uniforms.vars.UniformVar class ScriptVariables(private val placeHolder: String): Iterable { private val variables = ArrayList() diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/script/Shader.kt b/application-interface/src/main/kotlin/com/mechanica/engine/shader/script/Shader.kt similarity index 51% rename from desktop-application/src/main/kotlin/com/mechanica/engine/shader/script/Shader.kt rename to application-interface/src/main/kotlin/com/mechanica/engine/shader/script/Shader.kt index e8099174..355441bb 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/script/Shader.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/shader/script/Shader.kt @@ -1,29 +1,37 @@ package com.mechanica.engine.shader.script +import com.mechanica.engine.context.loader.GLLoader import com.mechanica.engine.models.Bindable import com.mechanica.engine.models.Model -import org.lwjgl.opengl.GL20 abstract class Shader { + abstract val id: Int + abstract val vertex: ShaderScript abstract val fragment: ShaderScript - abstract val tessellation: ShaderScript? - abstract val geometry: ShaderScript? + open val tessellation: ShaderScript? = null + open val geometry: ShaderScript? = null - abstract val id: Int + private var locationsFound = false protected fun load() { - GL20.glUseProgram(id) + loadProgram(id) loadUniforms() } + protected abstract fun loadProgram(id: Int) + private fun loadUniforms() { + if (!locationsFound) findLocations() + vertex.loadVariables() fragment.loadVariables() tessellation?.loadVariables() geometry?.loadVariables() } + abstract fun loadUniformLocation(name: String): Int + open fun render(inputs: Array, draw: () -> Unit) { load() @@ -40,13 +48,28 @@ abstract class Shader { model.draw() } + private fun findLocations() { + vertex.loadUniformLocations(this) + geometry?.loadUniformLocations(this) + fragment.loadUniformLocations(this) + locationsFound = true + } + companion object { operator fun invoke( vertex: ShaderScript, fragment: ShaderScript, tessellation: ShaderScript? = null, geometry: ShaderScript? = null): Shader { - return ShaderImpl(vertex, fragment, tessellation, geometry) + return create(vertex, fragment, tessellation, geometry) + } + + fun create( + vertex: ShaderScript, + fragment: ShaderScript, + tessellation: ShaderScript? = null, + geometry: ShaderScript? = null): Shader { + return GLLoader.defaultShader(vertex, fragment, tessellation, geometry) } } } \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/script/ShaderDeclarations.kt b/application-interface/src/main/kotlin/com/mechanica/engine/shader/script/ShaderDeclarations.kt similarity index 94% rename from desktop-application/src/main/kotlin/com/mechanica/engine/shader/script/ShaderDeclarations.kt rename to application-interface/src/main/kotlin/com/mechanica/engine/shader/script/ShaderDeclarations.kt index bdb11bd3..c9474986 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/script/ShaderDeclarations.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/shader/script/ShaderDeclarations.kt @@ -1,7 +1,7 @@ package com.mechanica.engine.shader.script import com.mechanica.engine.shader.qualifiers.Attribute -import com.mechanica.engine.shader.vars.uniforms.UniformVars +import com.mechanica.engine.shader.uniforms.UniformVars import com.mechanica.engine.shader.qualifiers.Qualifier import com.mechanica.engine.shader.qualifiers.Uniform import com.mechanica.engine.shader.vars.ShaderVariable diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/script/ShaderScript.kt b/application-interface/src/main/kotlin/com/mechanica/engine/shader/script/ShaderScript.kt similarity index 72% rename from desktop-application/src/main/kotlin/com/mechanica/engine/shader/script/ShaderScript.kt rename to application-interface/src/main/kotlin/com/mechanica/engine/shader/script/ShaderScript.kt index bcfd8658..75bf6b3e 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/script/ShaderScript.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/shader/script/ShaderScript.kt @@ -1,15 +1,12 @@ package com.mechanica.engine.shader.script import com.mechanica.engine.models.Bindable -import com.mechanica.engine.shader.vars.uniforms.vars.UniformVar -import org.lwjgl.opengl.GL20 +import com.mechanica.engine.shader.uniforms.vars.UniformVar abstract class ShaderScript : ShaderDeclarations("autoVal") { val script: String get() = generateScript() - private var programId: Int = 0 - abstract val main: String private val inputs: Array = Array(20) { null } @@ -22,11 +19,10 @@ abstract class ShaderScript : ShaderDeclarations("autoVal") { return sb.toString() } - internal fun loadProgram(programId: Int) { - this.programId = programId + fun loadUniformLocations(shader: Shader) { for (v in iterator) { if (v is UniformVar<*>) { - v.location = GL20.glGetUniformLocation(programId, v.locationName) + v.location = shader.loadUniformLocation(v.locationName) } } } diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/UniformVars.kt b/application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/UniformVars.kt similarity index 93% rename from application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/UniformVars.kt rename to application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/UniformVars.kt index 96e0197d..468f47a7 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/UniformVars.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/UniformVars.kt @@ -1,7 +1,7 @@ -package com.mechanica.engine.shader.vars.uniforms +package com.mechanica.engine.shader.uniforms import com.mechanica.engine.color.Color -import com.mechanica.engine.shader.vars.uniforms.vars.* +import com.mechanica.engine.shader.uniforms.vars.* import com.mechanica.engine.unit.vector.Vector import org.joml.Matrix4f import org.joml.Vector3f diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformFloat.kt b/application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformFloat.kt similarity index 57% rename from application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformFloat.kt rename to application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformFloat.kt index 5fa2c417..bfce2497 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformFloat.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformFloat.kt @@ -1,7 +1,9 @@ -package com.mechanica.engine.shader.vars.uniforms.vars +package com.mechanica.engine.shader.uniforms.vars import com.mechanica.engine.shader.qualifiers.Qualifier import com.mechanica.engine.shader.vars.ShaderType +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty abstract class UniformFloat( override var value: Float, @@ -10,4 +12,7 @@ abstract class UniformFloat( ) : UniformVar() { override val type = ShaderType.float() + override fun setValue(thisRef: Any, property: KProperty<*>, value: Float) { + this.value = value + } } \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformMatrix4f.kt b/application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformMatrix4f.kt similarity index 72% rename from application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformMatrix4f.kt rename to application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformMatrix4f.kt index 7c134fd2..1fa1a0a1 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformMatrix4f.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformMatrix4f.kt @@ -1,8 +1,9 @@ -package com.mechanica.engine.shader.vars.uniforms.vars +package com.mechanica.engine.shader.uniforms.vars import com.mechanica.engine.shader.qualifiers.Qualifier import com.mechanica.engine.shader.vars.ShaderType import org.joml.Matrix4f +import kotlin.reflect.KProperty abstract class UniformMatrix4f( var matrix: Matrix4f, @@ -19,4 +20,8 @@ abstract class UniformMatrix4f( fun set(matrix: Matrix4f) { this.matrix.set(matrix) } + + override fun setValue(thisRef: Any, property: KProperty<*>, value: Matrix4f) { + set(value) + } } \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformVar.kt b/application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformVar.kt similarity index 55% rename from application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformVar.kt rename to application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformVar.kt index 55378946..209e0dce 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformVar.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformVar.kt @@ -1,10 +1,13 @@ -package com.mechanica.engine.shader.vars.uniforms.vars +package com.mechanica.engine.shader.uniforms.vars import com.mechanica.engine.shader.qualifiers.Qualifier import com.mechanica.engine.shader.vars.ShaderType import com.mechanica.engine.shader.vars.ShaderVariable +import kotlin.properties.ReadOnlyProperty +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty -abstract class UniformVar : ShaderVariable { +abstract class UniformVar : ShaderVariable, ReadWriteProperty { abstract val value: T abstract override val name: String abstract override val qualifier: Qualifier @@ -13,14 +16,21 @@ abstract class UniformVar : ShaderVariable { var location = 0 - private val regex = Regex("[^\\w\\d]") val locationName: String - get() { + get() = getLocationName(name) + + override fun toString() = name + + override fun getValue(thisRef: Any, property: KProperty<*>) = value + + companion object { + private val regex = Regex("[^\\w\\d]") + + fun getLocationName(name: String): String { val index = regex.find(name)?.range?.first return if (index != null) { name.substring(0 until index) } else name } - - override fun toString() = name + } } \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformVector2f.kt b/application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformVector2f.kt similarity index 70% rename from application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformVector2f.kt rename to application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformVector2f.kt index 52e619ec..29d46e35 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformVector2f.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformVector2f.kt @@ -1,8 +1,9 @@ -package com.mechanica.engine.shader.vars.uniforms.vars +package com.mechanica.engine.shader.uniforms.vars import com.mechanica.engine.shader.qualifiers.Qualifier import com.mechanica.engine.shader.vars.ShaderType import com.mechanica.engine.unit.vector.DynamicVector +import kotlin.reflect.KProperty abstract class UniformVector2f( x: Number, y: Number, @@ -11,4 +12,8 @@ abstract class UniformVector2f( ) : UniformVar(), DynamicVector by DynamicVector.create(x.toDouble(), y.toDouble()) { override val value: DynamicVector by lazy { this } override val type = ShaderType.vec2() + + override fun setValue(thisRef: Any, property: KProperty<*>, value: DynamicVector) { + this.value.set(value) + } } \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformVector3f.kt b/application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformVector3f.kt similarity index 73% rename from application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformVector3f.kt rename to application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformVector3f.kt index 9ed2985c..88355450 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformVector3f.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformVector3f.kt @@ -1,8 +1,9 @@ -package com.mechanica.engine.shader.vars.uniforms.vars +package com.mechanica.engine.shader.uniforms.vars import com.mechanica.engine.shader.qualifiers.Qualifier import com.mechanica.engine.shader.vars.ShaderType import org.joml.Vector3f +import kotlin.reflect.KProperty abstract class UniformVector3f( x: Number, y: Number, z: Number, @@ -19,4 +20,8 @@ abstract class UniformVector3f( value.y = y.toFloat() value.z = z.toFloat() } + + override fun setValue(thisRef: Any, property: KProperty<*>, value: Vector3f) { + this.value.set(value) + } } \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformVector4f.kt b/application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformVector4f.kt similarity index 84% rename from application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformVector4f.kt rename to application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformVector4f.kt index 5a99ad2e..84674ba4 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/vars/UniformVector4f.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/shader/uniforms/vars/UniformVector4f.kt @@ -1,10 +1,11 @@ -package com.mechanica.engine.shader.vars.uniforms.vars +package com.mechanica.engine.shader.uniforms.vars import com.mechanica.engine.color.Color import com.mechanica.engine.color.LightweightColor import com.mechanica.engine.shader.qualifiers.Qualifier import com.mechanica.engine.shader.vars.ShaderType import org.joml.Vector4f +import kotlin.reflect.KProperty abstract class UniformVector4f ( @@ -37,4 +38,8 @@ abstract class UniformVector4f ( value.z = z.toFloat() value.w = w.toFloat() } + + override fun setValue(thisRef: Any, property: KProperty<*>, value: Vector4f) { + this.value.set(value) + } } \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/text/Font.kt b/application-interface/src/main/kotlin/com/mechanica/engine/text/Font.kt index 73c71125..670373f8 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/text/Font.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/text/Font.kt @@ -1,7 +1,7 @@ package com.mechanica.engine.text -import com.mechanica.engine.models.Image import com.mechanica.engine.context.loader.GLLoader +import com.mechanica.engine.models.Image import com.mechanica.engine.resources.Resource import com.mechanica.engine.unit.vector.DynamicVector @@ -17,8 +17,11 @@ abstract class Font { abstract fun addCharacterDataToArrays(cursor: CharacterCursor, positions: Array, texCoords: Array) companion object { - fun create(resource: Resource): Font { - return GLLoader.fontLoader.font(resource) + + val defaultFont = GLLoader.fontLoader.defaultFont + + fun create(resource: Resource, configureAtlas: FontAtlasConfiguration.() -> Unit = { }): Font { + return GLLoader.fontLoader.font(resource, configureAtlas) } } } \ No newline at end of file diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/text/FontAtlasConfiguration.kt b/application-interface/src/main/kotlin/com/mechanica/engine/text/FontAtlasConfiguration.kt new file mode 100644 index 00000000..04f2dc64 --- /dev/null +++ b/application-interface/src/main/kotlin/com/mechanica/engine/text/FontAtlasConfiguration.kt @@ -0,0 +1,16 @@ +package com.mechanica.engine.text + +interface FontAtlasConfiguration { + var width: Int + var height: Int + var characterSize: Float + var padding: Float + var charRange: CharRange + fun configureSDF(block: SDFConfiguration.() -> Unit) + + class SDFConfiguration { + var start = -20.0 + var end = 20.0 + } +} + diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/text/Text.kt b/application-interface/src/main/kotlin/com/mechanica/engine/text/Text.kt index f782aeb0..3cb4fa03 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/text/Text.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/text/Text.kt @@ -8,7 +8,7 @@ import kotlin.math.max class Text(text: String, val font: Font = GLLoader.fontLoader.defaultFont) { - var text = text + var string = text set(value) { field = value update(value, font) diff --git a/application-interface/src/main/kotlin/com/mechanica/engine/vertices/VertexBufferObjects.kt b/application-interface/src/main/kotlin/com/mechanica/engine/vertices/VertexBufferObjects.kt index a7b41c46..31a0cb1f 100644 --- a/application-interface/src/main/kotlin/com/mechanica/engine/vertices/VertexBufferObjects.kt +++ b/application-interface/src/main/kotlin/com/mechanica/engine/vertices/VertexBufferObjects.kt @@ -13,6 +13,16 @@ interface VertexBuffer : Bindable { val vertexCount: Int val T.valueCount: Int + fun safeBind() { + if (vertexCount > 0) { + bind() + } else { + unbind() + } + } + + fun unbind() + fun set(array: T, from: Int = 0, length: Int = array.valueCount) } @@ -21,7 +31,6 @@ interface AttributeArray : VertexBuffer { fun set(array: Array, from: Int = 0, length: Int = array.size) fun set(array: Array, from: Int = 0, length: Int = array.size) fun set(array: Array, from: Int = 0, length: Int = array.size) - fun disable() } interface IndexArray : VertexBuffer { diff --git a/build.gradle.kts b/build.gradle.kts index e4270188..27f9b75e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile val lwjglVersion = "3.2.3" val lwjglNatives = "natives-windows" -val kotlinVersion = "1.3.50" +val kotlinVersion = "1.3.70" buildscript { repositories { @@ -18,7 +18,8 @@ buildscript { } plugins { - id("org.jetbrains.kotlin.jvm") version "1.3.61" + kotlin("jvm") version "1.3.70" + kotlin("plugin.serialization") version "1.3.70" `java-library` maven } @@ -28,6 +29,7 @@ val commonDependencies: DependencyHandlerScope.() -> Unit = { // Align versions of all Kotlin components implementation(platform("org.jetbrains.kotlin:kotlin-bom")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") @@ -69,6 +71,16 @@ allprojects { apply(plugin = "org.jetbrains.kotlin.jvm") dependencies(commonDependencies) + val compileKotlin: KotlinCompile by tasks + compileKotlin.kotlinOptions { + freeCompilerArgs = listOf("-XXLanguage:+InlineClasses") + jvmTarget = "12" + } + + java { + sourceCompatibility = JavaVersion.VERSION_12 + targetCompatibility = JavaVersion.VERSION_12 + } } project(":desktop-application") { @@ -88,14 +100,3 @@ project(":samples") { apply(plugin = "org.jetbrains.kotlin.jvm") dependencies(coreLwjgl) } - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -val compileKotlin: KotlinCompile by tasks -compileKotlin.kotlinOptions { - freeCompilerArgs = listOf("-XXLanguage:+InlineClasses") - jvmTarget = "1.8" -} \ No newline at end of file diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 891ebe85..b556fdbe 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -2,9 +2,4 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") -} - -val compileKotlin: KotlinCompile by tasks -compileKotlin.kotlinOptions { - freeCompilerArgs = listOf("-XXLanguage:+InlineClasses") } \ No newline at end of file diff --git a/common/src/main/kotlin/com/mechanica/engine/color/ColorTools.kt b/common/src/main/kotlin/com/mechanica/engine/color/ColorTools.kt index 72c894c3..b1e670bd 100644 --- a/common/src/main/kotlin/com/mechanica/engine/color/ColorTools.kt +++ b/common/src/main/kotlin/com/mechanica/engine/color/ColorTools.kt @@ -2,9 +2,9 @@ package com.mechanica.engine.color -import com.mechanica.engine.unit.angle.degrees -import com.mechanica.engine.unit.angle.Angle import com.mechanica.engine.unit.angle.Degree +import com.mechanica.engine.unit.angle.Radian +import com.mechanica.engine.unit.angle.degrees import org.joml.Vector4f import kotlin.math.abs import kotlin.math.max @@ -182,14 +182,22 @@ fun rgb2Lightness(r: Double, g: Double, b: Double): Double { return (max + min)/2.0 } -fun hsl(hue: Angle, saturation: Double, lightness: Double, alpha: Double = 1.0): LightweightColor { +fun hsl(hue: Degree, saturation: Double, lightness: Double, alpha: Double = 1.0): LightweightColor { + return hsl(hue.toDouble(), saturation, lightness, alpha) +} + +fun hsl(hue: Radian, saturation: Double, lightness: Double, alpha: Double = 1.0): LightweightColor { + return hsl(hue.toDegrees().toDouble(), saturation, lightness, alpha) +} + +fun hsl(hue: Double, saturation: Double, lightness: Double, alpha: Double = 1.0): LightweightColor { fun f(n: Int, h: Double, s: Double, l: Double): Double { val a = s* min(l, 1.0-l) val k = (n + h/30.0)%12 return (l - a* max(min(min(k-3.0, 9.0-k), 1.0),-1.0)) } - val h = (hue.toDegrees().asDouble() + 360.0)%360.0 + val h = (hue + 360.0)%360.0 val s = saturation val l = lightness diff --git a/common/src/main/kotlin/com/mechanica/engine/color/DynamicColor.kt b/common/src/main/kotlin/com/mechanica/engine/color/DynamicColor.kt index 564be21f..e792c0a9 100644 --- a/common/src/main/kotlin/com/mechanica/engine/color/DynamicColor.kt +++ b/common/src/main/kotlin/com/mechanica/engine/color/DynamicColor.kt @@ -21,6 +21,14 @@ class DynamicColor( set(color.r, color.g, color.b, color.a) } + fun set(hex: Long) { + val r = hex2Red(hex) + val g = hex2Green(hex) + val b = hex2Blue(hex) + val a = hex2Alpha(hex) + set(r, g, b, a) + } + override fun toLong(): Long { return rgba2Hex(r, g, b, a) } diff --git a/common/src/main/kotlin/com/mechanica/engine/debug/DebugConfiguration.kt b/common/src/main/kotlin/com/mechanica/engine/debug/DebugConfiguration.kt index b40bb568..4475d5e9 100644 --- a/common/src/main/kotlin/com/mechanica/engine/debug/DebugConfiguration.kt +++ b/common/src/main/kotlin/com/mechanica/engine/debug/DebugConfiguration.kt @@ -6,4 +6,6 @@ interface DebugConfiguration { val constructionDraws: Boolean val printWarnings: Boolean val lwjglDebug: Boolean + fun pauseUpdates(pause: Boolean) + fun frameAdvance() } \ No newline at end of file diff --git a/common/src/main/kotlin/com/mechanica/engine/memory/BitmapBuffer.kt b/common/src/main/kotlin/com/mechanica/engine/memory/BitmapBuffer.kt new file mode 100644 index 00000000..6f853029 --- /dev/null +++ b/common/src/main/kotlin/com/mechanica/engine/memory/BitmapBuffer.kt @@ -0,0 +1,110 @@ +package com.mechanica.engine.memory + +import com.mechanica.engine.unit.vector.DynamicIntVector +import java.nio.ByteBuffer + +class BitmapBuffer(val buffer: ByteBuffer, val width: Int, val height: Int) { + + val bufferPosition = object : DynamicIntVector { + private var _x = 0 + override var x: Int + get() = _x + set(value) { + set(value, _y) + } + + private var _y = 0 + override var y: Int + get() = _y + set(value) { + set(_x, value) + } + + override fun set(x: Int, y: Int) { + _x = x + _y = y + val row = y*width + buffer.position(row+x) + } + } + + val stBufferPosition = STVector() + + fun advanceBy(advance: Int) { + buffer.position(buffer.position() + advance) + } + + fun insert(other: ByteBuffer, otherWidth: Int, s: Float, t: Float) { + stBufferPosition.set(s, t) + insert(other, otherWidth) + } + + fun insert(other: ByteBuffer, otherWidth: Int, x: Int, y: Int) { + bufferPosition.set(x, y) + insert(other, otherWidth) + } + + fun insert(other: BitmapBuffer, s: Float, t: Float) { + stBufferPosition.set(s, t) + other.zero() + + insert(other.buffer, other.width) + } + + fun insert(other: BitmapBuffer, x: Int, y: Int) { + bufferPosition.set(x, y) + other.zero() + + insert(other.buffer, other.width) + } + + fun zero() { + buffer.position(0) + } + + private fun insert(other: ByteBuffer, otherWidth: Int) { + var j = 0 + val limit = other.limit() + while (other.position() < limit - otherWidth) { + other.limit((j + 1)*otherWidth - 1) + other.position(j++*otherWidth) + buffer.put(other) + advanceBy(width - otherWidth + 1) + } + } + private fun insertOld(other: ByteBuffer, otherWidth: Int) { + var j = 0 + while (other.hasRemaining()) { + var i = 0 + while (i++ < otherWidth) { + buffer.put(other.get()) + } + advanceBy(width - otherWidth) + j++ + } + } + + inner class STVector { + private var _s = 0f + var s: Float + get() = _s + set(value) { + set(value, _t) + } + + private var _t = 0f + var t: Float + get() = _t + set(value) { + set(_s, value) + } + + fun set(s: Float, t: Float) { + _s = s + _t = t + val x = (s*width).toInt() + val y = (t*height).toInt() + bufferPosition.set(x, y) + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/mechanica/engine/unit/angle/Angle.kt b/common/src/main/kotlin/com/mechanica/engine/unit/angle/Angle.kt index a8c8a292..d6f81781 100644 --- a/common/src/main/kotlin/com/mechanica/engine/unit/angle/Angle.kt +++ b/common/src/main/kotlin/com/mechanica/engine/unit/angle/Angle.kt @@ -1,7 +1,8 @@ package com.mechanica.engine.unit.angle interface Angle { - fun asDouble(): Double + fun toDouble(): Double + fun toFloat(): Float fun toDegrees(): Degree fun toRadians(): Radian } \ No newline at end of file diff --git a/common/src/main/kotlin/com/mechanica/engine/unit/angle/AngleTools.kt b/common/src/main/kotlin/com/mechanica/engine/unit/angle/AngleTools.kt index 51282eed..abd694c4 100644 --- a/common/src/main/kotlin/com/mechanica/engine/unit/angle/AngleTools.kt +++ b/common/src/main/kotlin/com/mechanica/engine/unit/angle/AngleTools.kt @@ -1,45 +1,52 @@ package com.mechanica.engine.unit.angle +import kotlin.math.PI + inline val Number.degrees: Degree get() = Degree(this.toDouble()) inline val Number.radians: Radian get() = Radian(this.toDouble()) -operator fun Angle.minus(other: Angle): Angle = (this.toRadians().asDouble() - other.toRadians().asDouble()).radians +operator fun Angle.minus(other: Angle): Angle = (this.toRadians().toDouble() - other.toRadians().toDouble()).radians -operator fun Degree.unaryMinus(): Degree = Degree(-(this.asDouble())) +operator fun Degree.unaryMinus(): Degree = Degree(-(this.toDouble())) -operator fun Radian.unaryMinus(): Radian = Radian(-(this.asDouble())) +operator fun Radian.unaryMinus(): Radian = Radian(-(this.toDouble())) operator fun Degree.plus(other: Degree): Degree { - return (this.asDouble() + other.asDouble()).degrees + return (this.toDouble() + other.toDouble()).degrees } operator fun Radian.plus(other: Radian): Radian { - return (this.asDouble() + other.asDouble()).radians + return (this.toDouble() + other.toDouble()).radians } operator fun Degree.minus(other: Degree): Degree { - return (this.asDouble() - other.asDouble()).degrees + return (this.toDouble() - other.toDouble()).degrees } operator fun Radian.minus(other: Radian): Radian { - return (this.asDouble() - other.asDouble()).radians + return (this.toDouble() - other.toDouble()).radians } operator fun Degree.div(other: Degree): Double { - return (this.asDouble() / other.asDouble()) + return (this.toDouble() / other.toDouble()) } operator fun Radian.div(other: Radian): Double { - return (this.asDouble() / other.asDouble()) + return (this.toDouble() / other.toDouble()) } operator fun Degree.times(other: Number): Degree { - return (this.asDouble() * other.toDouble()).degrees + return (this.toDouble() * other.toDouble()).degrees } operator fun Radian.times(other: Number): Radian { - return (this.asDouble() * other.toDouble()).radians + return (this.toDouble() * other.toDouble()).radians +} + +fun minimumDifference(angle1: Radian, angle2: Radian): Radian { + val diff = angle2 - angle1 + return ((diff.toDouble() + PI) % (2.0* PI) - PI).radians } diff --git a/common/src/main/kotlin/com/mechanica/engine/unit/angle/Degree.kt b/common/src/main/kotlin/com/mechanica/engine/unit/angle/Degree.kt index 8b12a903..fcf47c0b 100644 --- a/common/src/main/kotlin/com/mechanica/engine/unit/angle/Degree.kt +++ b/common/src/main/kotlin/com/mechanica/engine/unit/angle/Degree.kt @@ -3,13 +3,15 @@ package com.mechanica.engine.unit.angle import kotlin.math.PI inline class Degree(private val degrees: Double) : Angle { - override fun toDegrees() = asDouble().degrees + override fun toDegrees() = toDouble().degrees - override fun toRadians() = (this.asDouble()*PI /180.0).radians + override fun toRadians() = (this.toDouble()*PI /180.0).radians - override fun asDouble() = degrees + override fun toDouble() = degrees + + override fun toFloat() = degrees.toFloat() override fun toString(): String { - return asDouble().toString() + return toDouble().toString() } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/mechanica/engine/unit/angle/Radian.kt b/common/src/main/kotlin/com/mechanica/engine/unit/angle/Radian.kt index 7131f7a4..fe0320b0 100644 --- a/common/src/main/kotlin/com/mechanica/engine/unit/angle/Radian.kt +++ b/common/src/main/kotlin/com/mechanica/engine/unit/angle/Radian.kt @@ -3,13 +3,15 @@ package com.mechanica.engine.unit.angle import kotlin.math.PI inline class Radian(private val radians: Double) : Angle { - override fun toDegrees() = (this.asDouble()*180.0/ PI).degrees + override fun toDegrees() = (this.toDouble()*180.0/ PI).degrees - override fun toRadians() = asDouble().radians + override fun toRadians() = toDouble().radians - override fun asDouble() = radians + override fun toDouble() = radians + + override fun toFloat() = radians.toFloat() override fun toString(): String { - return asDouble().toString() + return toDouble().toString() } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/mechanica/engine/unit/vector/DynamicIntVector.kt b/common/src/main/kotlin/com/mechanica/engine/unit/vector/DynamicIntVector.kt new file mode 100644 index 00000000..3eb4c4a8 --- /dev/null +++ b/common/src/main/kotlin/com/mechanica/engine/unit/vector/DynamicIntVector.kt @@ -0,0 +1,20 @@ +package com.mechanica.engine.unit.vector + +interface DynamicIntVector : IntVector { + override var x: Int + override var y: Int + + fun set(x: Int, y: Int) { + this.x = x + this.y = y + } + + fun set(other: IntVector) = set(other.x, other.y) + + companion object { + fun create(x: Int = 0, y: Int = 0) = object : DynamicIntVector { + override var x = x + override var y = y + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/mechanica/engine/unit/vector/IntVector.kt b/common/src/main/kotlin/com/mechanica/engine/unit/vector/IntVector.kt new file mode 100644 index 00000000..52c34953 --- /dev/null +++ b/common/src/main/kotlin/com/mechanica/engine/unit/vector/IntVector.kt @@ -0,0 +1,6 @@ +package com.mechanica.engine.unit.vector + +interface IntVector { + val x: Int + val y: Int +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/mechanica/engine/unit/vector/VectorTools.kt b/common/src/main/kotlin/com/mechanica/engine/unit/vector/VectorTools.kt index 652d76d6..5ce3e100 100644 --- a/common/src/main/kotlin/com/mechanica/engine/unit/vector/VectorTools.kt +++ b/common/src/main/kotlin/com/mechanica/engine/unit/vector/VectorTools.kt @@ -2,7 +2,6 @@ package com.mechanica.engine.unit.vector import com.mechanica.engine.unit.angle.Angle -import com.mechanica.engine.unit.angle.radians import kotlin.math.* @@ -19,8 +18,8 @@ fun vec(x: Number, y: Number): LightweightVector { } fun vec(r: Number, theta: Angle): LightweightVector { - val x = r.toDouble()*cos(theta.toRadians().asDouble()) - val y = r.toDouble()*sin(theta.toRadians().asDouble()) + val x = r.toDouble()*cos(theta.toRadians().toDouble()) + val y = r.toDouble()*sin(theta.toRadians().toDouble()) return vec(x, y) } diff --git a/common/src/main/kotlin/com/mechanica/engine/util/GameBoolean.kt b/common/src/main/kotlin/com/mechanica/engine/util/EventBoolean.kt similarity index 89% rename from common/src/main/kotlin/com/mechanica/engine/util/GameBoolean.kt rename to common/src/main/kotlin/com/mechanica/engine/util/EventBoolean.kt index e44220b4..750ba889 100644 --- a/common/src/main/kotlin/com/mechanica/engine/util/GameBoolean.kt +++ b/common/src/main/kotlin/com/mechanica/engine/util/EventBoolean.kt @@ -1,11 +1,11 @@ package com.mechanica.engine.util -abstract class GameBoolean { - fun update(delta: Double) { +open class EventBoolean(private val condition: () -> Boolean) { + fun update() { isTrue = condition() } - abstract fun condition() : Boolean + fun condition() : Boolean = condition.invoke() var isTrue: Boolean = false private set(value) { diff --git a/common/src/main/kotlin/com/mechanica/engine/util/GenericGameBoolean.kt b/common/src/main/kotlin/com/mechanica/engine/util/GenericGameBoolean.kt deleted file mode 100644 index 5922fea3..00000000 --- a/common/src/main/kotlin/com/mechanica/engine/util/GenericGameBoolean.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.mechanica.engine.util - -class GenericGameBoolean(private val condition: () -> Boolean) : GameBoolean() { - override fun condition(): Boolean { - return condition.invoke() - } -} \ No newline at end of file diff --git a/common/src/main/kotlin/com/mechanica/engine/util/JsonUtil.kt b/common/src/main/kotlin/com/mechanica/engine/util/JsonUtil.kt new file mode 100644 index 00000000..1c68c20a --- /dev/null +++ b/common/src/main/kotlin/com/mechanica/engine/util/JsonUtil.kt @@ -0,0 +1,73 @@ +package com.mechanica.engine.util + +import kotlinx.serialization.* +import kotlinx.serialization.json.* + + +fun Map<*, *>.toJson(): JsonObject { + return json { + for (entry in this@toJson) { + addSupportedType(entry.key.toString(), entry.value) + } + } +} + +fun JsonObject.toStringValueMap(): Map { + return this.mapNotNull { + val value = it.value.getSupportedType() + + if (value != null) { + it.key to value + } else null + + }.toHashMap() +} + +fun List>.toHashMap(): HashMap { + val map = HashMap() + for (p in this) { + map[p.first] = p.second + } + return map +} + +fun JsonObjectBuilder.addSupportedType(key: String, value: V) { + when (value) { + is Number -> key to value + is String -> key to value + is Boolean -> key to value + is JsonElement -> key to value + is Map<*, *> -> key to value.toJson() + } +} + +fun JsonElement.getSupportedType(): Any? { + return try { + booleanOrNull ?: intOrNull ?: longOrNull ?: floatOrNull ?: doubleOrNull ?: contentOrNull + } catch (ex: JsonException) { + if (this is JsonObject) { + return this.toStringValueMap() + } else null + } +} + +fun Decoder.toJsonObject(): JsonObject { + val input = this as? JsonInput ?: throw SerializationException("Expected Json Input") + return input.decodeJson() as? JsonObject ?: throw SerializationException("Expected JsonArray") +} + + +class StringMapSerializer : DeserializationStrategy>, SerializationStrategy> { + + override val descriptor = SerialDescriptor("PersistenceMap") + + override fun deserialize(decoder: Decoder) = decoder.toJsonObject().toStringValueMap() + + override fun serialize(encoder: Encoder, value: Map) { + JsonElementSerializer.serialize(encoder, value.toJson()) + } + + override fun patch(decoder: Decoder, old: Map): Map = + throw UpdateNotSupportedException("Update not supported") + +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/mechanica/engine/util/NullTerminatedChars.kt b/common/src/main/kotlin/com/mechanica/engine/util/NullTerminatedChars.kt new file mode 100644 index 00000000..b071375f --- /dev/null +++ b/common/src/main/kotlin/com/mechanica/engine/util/NullTerminatedChars.kt @@ -0,0 +1,77 @@ +package com.mechanica.engine.util + +import kotlin.math.min + +class NullTerminatedChars(capacity: Int = 50) { + val chars = CharArray(capacity) { Character.MIN_VALUE } + var size: Int = 0 + + fun add(char: Char) { + var index = 0 + + while (index < chars.size) { + if (chars[index] == Character.MIN_VALUE) { + chars[index] = char + break + } + index++ + } + size = index+1 + } + + fun addCodePoint(cp: Int) { + when { + Character.isBmpCodePoint(cp) -> { + add(cp.toChar()) + } + Character.isValidCodePoint(cp) -> { + add(Character.highSurrogate(cp)) + add(Character.lowSurrogate(cp)) + } + } + } + + fun get(other: NullTerminatedChars) { + get(other.chars) + } + + fun get(other: CharArray) { + val endIndex = min(other.size, this.chars.size) + this.chars.copyInto(other, endIndex = endIndex) + if (endIndex < other.size) { + other.fill(Character.MIN_VALUE, endIndex, other.lastIndex) + } + } + + fun get(sb: StringBuilder, offset: Int = -1): Int { + var count = 0 + forEach { + if (offset < 0) { + sb.append(it) + } else { + sb.insert(offset+count, it) + } + count++ + } + return count + } + + fun clear() { + size = 0 + set { Character.MIN_VALUE } + } + + inline fun forEach(operation: (Char) -> Unit) { + set { operation(it); it } + } + + inline fun set(operation: (Char) -> Char) { + var i = 0 + var currentChar = chars.first() + + while (currentChar != Character.MIN_VALUE && i < chars.size - 1) { + chars[i] = operation(currentChar) + currentChar = chars[++i] + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/mechanica/engine/util/extensions/Arrays.kt b/common/src/main/kotlin/com/mechanica/engine/util/extensions/Arrays.kt index 98530a1f..a5e67492 100644 --- a/common/src/main/kotlin/com/mechanica/engine/util/extensions/Arrays.kt +++ b/common/src/main/kotlin/com/mechanica/engine/util/extensions/Arrays.kt @@ -118,6 +118,25 @@ fun FloatArray.fill(iterator: Iterator) { } } + +/** + * A version of for each that should be more efficient for lists that + * are fast at getting element with random access + * + * @param action action that should be performed on each element + */ +inline fun Array.fori(action: (E) -> Unit) { + for (i in this.indices) { + action(this[i]) + } +} + +inline fun Array.foriIndexed(action: (E, Int) -> Unit) { + for (i in this.indices) { + action(this[i], i) + } +} + fun List<*>.indexLooped(index: Int): Int { return when { index < 0 -> this.size + index diff --git a/common/src/main/kotlin/com/mechanica/engine/util/extensions/Iterables.kt b/common/src/main/kotlin/com/mechanica/engine/util/extensions/Iterables.kt index 1a1071f6..4cfa5e58 100644 --- a/common/src/main/kotlin/com/mechanica/engine/util/extensions/Iterables.kt +++ b/common/src/main/kotlin/com/mechanica/engine/util/extensions/Iterables.kt @@ -14,4 +14,22 @@ fun Iterable.containsPoint(point: Vector): Boolean { prev = vec } return result +} + +/** + * A version of for each that should be more efficient for lists that + * are fast at getting element with random access + * + * @param action action that should be performed on each element + */ +inline fun List.fori(action: (E) -> Unit) { + for (i in this.indices) { + action(this[i]) + } +} + +inline fun List.foriIndexed(action: (E, Int) -> Unit) { + for (i in this.indices) { + action(this[i], i) + } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/mechanica/engine/util/extensions/Numbers.kt b/common/src/main/kotlin/com/mechanica/engine/util/extensions/Numbers.kt index 3a05f536..8436af64 100644 --- a/common/src/main/kotlin/com/mechanica/engine/util/extensions/Numbers.kt +++ b/common/src/main/kotlin/com/mechanica/engine/util/extensions/Numbers.kt @@ -3,6 +3,7 @@ package com.mechanica.engine.util.extensions import kotlin.math.max import kotlin.math.min +import kotlin.math.sign fun Number.isHigher(other: Number): Boolean { return this.toDouble() > other.toDouble() @@ -55,8 +56,29 @@ fun Number.isLower(other1: Number, other2: Number, other3: Number, vararg others } fun Double.constrain(lower: Double, higher: Double): Double { - val restrainLower = max(this, lower) - return min(restrainLower, higher) + val sign = sign(higher - lower) + if (sign == 0.0) return lower + + val restrainLower = max(sign*this, sign*lower) + return sign*min(restrainLower, sign*higher) +} + +fun Double.constrainLooped(lower: Double, higher: Double): Double { + val diff = (higher - lower) + val sign = sign(diff) + + if (sign == 0.0) return lower + + return when { + sign*this < sign*lower -> higher + sign * (sign * this - lower) % diff + sign*this > sign*higher -> lower + sign * (sign * this - higher) % diff + else -> this + } +} + +fun main() { + val num = (7.0).constrain(5.0, 5.0) + println(num) } fun Int.constrain(lower: Int, higher: Int): Int { diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/audio/ALAudioObject.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/audio/ALAudioObject.kt new file mode 100644 index 00000000..9f66548a --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/audio/ALAudioObject.kt @@ -0,0 +1,39 @@ +package com.mechanica.engine.audio + +import com.mechanica.engine.memory.useMemoryStack +import org.joml.Vector3f +import org.lwjgl.openal.AL10 + +abstract class ALAudioObject : AudioObject { + private val _position: Vector3f by lazy { createVector3fSetterInterept(AL10.AL_POSITION, this::set3f) } + override var position: Vector3f + get() { + getVector3f(AL10.AL_POSITION, _position) + return _position + } + set(value) { + _position.set(value) + } + + private val _velocity: Vector3f by lazy { createVector3fSetterInterept(AL10.AL_VELOCITY, this::set3f) } + override var velocity: Vector3f + get() { + getVector3f(AL10.AL_VELOCITY, _velocity) + return _velocity + } + set(value) { + _velocity.set(value) + } + + protected abstract fun getVector3f(property: Int, vec: Vector3f) + + protected abstract fun set3f(property: Int, x: Float, y: Float, z: Float) + + + protected fun createVector3fSetterInterept(property: Int, setter: (Int, Float, Float, Float) -> Unit) = object : Vector3f() { + override fun set(x: Float, y: Float, z: Float): Vector3f { + setter(property, x, y, z) + return super.set(x, y, z) + } + } +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/audio/ALListener.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/audio/ALListener.kt new file mode 100644 index 00000000..fe3f25cb --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/audio/ALListener.kt @@ -0,0 +1,82 @@ +package com.mechanica.engine.audio + +import com.mechanica.engine.memory.useMemoryStack +import org.joml.Vector3f +import org.lwjgl.openal.AL10.* +import java.nio.FloatBuffer + +class ALListener : ALAudioObject(), Listener { + override var gain: Float + get() = alGetListenerf(AL_GAIN) + set(value) { alListenerf(AL_GAIN, value )} + + override val orientation = ALOrientation() + + override var distanceModel: Listener.DistanceModel = Listener.DistanceModel.INVERSE.also { alDistanceModel(distanceModelEnumToALNum(it)) } + set(value) { alDistanceModel(distanceModelEnumToALNum(value)) } + + override fun getVector3f(property: Int, vec: Vector3f) { + useMemoryStack { + val floats = floats(0f, 0f, 0f) + alGetListenerfv(property, floats) + vec.set(floats[0], floats[1], floats[2]) + } + } + + override fun set3f(property: Int, x: Float, y: Float, z: Float) { + alListener3f(property, x, y, z) + } + + inner class ALOrientation : Listener.Orientaion { + private val _at: Vector3f = createVector3fSetterInterept(AL_ORIENTATION, this::setAt) + override var at: Vector3f + get() { + get() + return _at + } + set(value) { + _at.set(value) + } + + private val _up: Vector3f = createVector3fSetterInterept(AL_ORIENTATION, this::setUp) + override var up: Vector3f + get() { + get() + return _up + } + set(value) { + _up.set(value) + } + + private fun get() { + useMemoryStack { + val floats = mallocFloat(6) + alGetListenerfv(AL_ORIENTATION, floats) + getValues(floats) + } + } + + private fun setUp(property: Int = AL_ORIENTATION, x: Float, y: Float, z: Float) = set(property, _at.x, _at.y, _at.z, x, y, z) + private fun setAt(property: Int = AL_ORIENTATION, x: Float, y: Float, z: Float) = set(property, x, y, z, _up.x, _up.y, _up.z) + + private fun set(property: Int, atX: Float, atY: Float, atZ: Float, upX: Float, upY: Float, upZ: Float) { + useMemoryStack { + val floats = mallocFloat(6) + floats.put(atX).put(atY).put(atZ) + .put(upX).put(upY).put(upZ) + alListenerfv(property, floats) + } + } + + private fun getValues(buffer: FloatBuffer) { + buffer.getVector3f(at) + buffer.getVector3f(up) + buffer.position(0) + } + + private fun FloatBuffer.getVector3f(vec: Vector3f) { + vec.set(get(), get(), get()) + } + } + +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/audio/ALSound.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/audio/ALSound.kt new file mode 100644 index 00000000..957ee8c1 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/audio/ALSound.kt @@ -0,0 +1,31 @@ +package com.mechanica.engine.audio + +import org.lwjgl.openal.AL10.* + +class ALSound(file: AudioFile) : Sound, AudioFile by file { + override val id: Int = generateBufferId() + override val frequency: Int + get() = alGetBufferi(id, AL_FREQUENCY) + + constructor(file: String) : this(OggAudioFile(file)) + + init { + alBufferData(id, format, buffer, sampleRate) + } + + override fun destroy() { + alDeleteBuffers(id) + } + + companion object { + fun generateBufferId(): Int { + alGetError() + val id = alGenBuffers() + val error = alGetError() + if (error != AL_NO_ERROR) { + throw IllegalStateException("Error generating OpenAL buffer, ERROR_CODE: $error") + } + return id + } + } +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/audio/ALSource.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/audio/ALSource.kt new file mode 100644 index 00000000..ed349345 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/audio/ALSource.kt @@ -0,0 +1,65 @@ +package com.mechanica.engine.audio + +import com.mechanica.engine.memory.useMemoryStack +import org.joml.Vector3f +import org.lwjgl.openal.AL10.* +import kotlin.reflect.KProperty + +class ALSource(override var sound: Sound) : ALAudioObject(), SoundSource { + override val id: Int = alGenSources() + + private val properties = ALSourceProperty(id) + + override var gain: Float by properties.createProperty(AL_GAIN) + override var pitch: Float by properties.createProperty(AL_PITCH) + override var rolloff: Float by properties.createProperty(AL_ROLLOFF_FACTOR) + override var maxDistance: Float by properties.createProperty(AL_MAX_DISTANCE) + override var referenceDistance: Float by properties.createProperty(AL_REFERENCE_DISTANCE) + + init { + alSourcei(id, AL_BUFFER, sound.id) + } + + override fun play() { + alSourcePlay(id) + } + + override fun pause() { + alSourcePause(id) + } + + override fun stop() { + alSourceStop(id) + } + + override fun destroy() { + alDeleteSources(id) + } + + + override fun getVector3f(property: Int, vec: Vector3f) { + useMemoryStack { + val floats = floats(0f, 0f, 0f) + alGetSourcefv(id, property, floats) + vec.set(floats[0], floats[1], floats[2]) + } + } + + override fun set3f(property: Int, x: Float, y: Float, z: Float) { + alSource3f(id, property, x, y, z) + } + + + private inner class ALFloatProperty(private val property: Int) { + operator fun getValue(thisRef: SoundSource, property: KProperty<*>): Float { + return getFloat(this.property) + } + + operator fun setValue(thisRef: SoundSource, property: KProperty<*>, value: Float) { + setFloat(this.property, value) + } + + private fun getFloat(property: Int) = alGetSourcef(id, property) + private fun setFloat(property: Int, float: Float) = alSourcef(id, property, float) + } +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/audio/ALSourceProperty.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/audio/ALSourceProperty.kt new file mode 100644 index 00000000..c87c2d47 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/audio/ALSourceProperty.kt @@ -0,0 +1,22 @@ +package com.mechanica.engine.audio + +import org.lwjgl.openal.AL10 +import kotlin.reflect.KProperty + +class ALSourceProperty(private val sourceId: Int) { + + fun createProperty(property: Int) = DelegatedProperty(property) + + private fun getFloat(property: Int) = AL10.alGetSourcef(sourceId, property) + private fun setFloat(property: Int, float: Float) = AL10.alSourcef(sourceId, property, float) + + inner class DelegatedProperty(private val property: Int) { + operator fun getValue(thisRef: SoundSource, property: KProperty<*>): Float { + return getFloat(this.property) + } + + operator fun setValue(thisRef: SoundSource, property: KProperty<*>, value: Float) { + setFloat(this.property, value) + } + } +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/audio/AudioUtils.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/audio/AudioUtils.kt new file mode 100644 index 00000000..73f75388 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/audio/AudioUtils.kt @@ -0,0 +1,23 @@ +package com.mechanica.engine.audio + +import org.lwjgl.openal.AL11.* + +fun distanceModelEnumToALNum(dm: Listener.DistanceModel): Int { + return when(dm) { + Listener.DistanceModel.INVERSE -> if (dm.clamped) AL_INVERSE_DISTANCE_CLAMPED else AL_INVERSE_DISTANCE + Listener.DistanceModel.LINEAR -> if (dm.clamped) AL_LINEAR_DISTANCE_CLAMPED else AL_LINEAR_DISTANCE + Listener.DistanceModel.EXPONENTIAL -> if (dm.clamped) AL_EXPONENT_DISTANCE_CLAMPED else AL_EXPONENT_DISTANCE + } +} + +fun distanceModelALNumToEnum(dm: Int): Listener.DistanceModel { + return when(dm) { + AL_INVERSE_DISTANCE -> Listener.DistanceModel.INVERSE.also { it.clamped = false } + AL_INVERSE_DISTANCE_CLAMPED -> Listener.DistanceModel.INVERSE.also { it.clamped = true } + AL_EXPONENT_DISTANCE -> Listener.DistanceModel.EXPONENTIAL.also { it.clamped = false } + AL_EXPONENT_DISTANCE_CLAMPED -> Listener.DistanceModel.EXPONENTIAL.also { it.clamped = true } + AL_LINEAR_DISTANCE -> Listener.DistanceModel.LINEAR.also { it.clamped = false } + AL_LINEAR_DISTANCE_CLAMPED -> Listener.DistanceModel.LINEAR.also { it.clamped = true } + else -> Listener.DistanceModel.INVERSE + } +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/audio/OggAudioFile.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/audio/OggAudioFile.kt new file mode 100644 index 00000000..eabb75b9 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/audio/OggAudioFile.kt @@ -0,0 +1,55 @@ +package com.mechanica.engine.audio + +import com.mechanica.engine.memory.useMemoryStack +import org.lwjgl.BufferUtils +import org.lwjgl.openal.AL10 +import org.lwjgl.stb.STBVorbis +import org.lwjgl.system.libc.LibCStdlib +import java.nio.ShortBuffer + +class OggAudioFile(fileName: String) : AudioFile { + override val channels: Int + override val sampleRate: Int + override val buffer: ShortBuffer + override val format: Int + get() = when (channels) { + 1 -> AL10.AL_FORMAT_MONO16 + 2 -> AL10.AL_FORMAT_STEREO16 + else -> -1 + } + + init { + var channels = -1 + var sampleRate = -1 + var ogg: ShortBuffer? = null + useMemoryStack { + val channelBuffer = ints(0) + val sampleRateBuffer = ints(0) + ogg = STBVorbis.stb_vorbis_decode_filename(fileName.removePrefix("/"), channelBuffer, sampleRateBuffer) + channels = channelBuffer[0] + sampleRate = sampleRateBuffer[0] + } + + this.buffer = convertToGCBuffer(ogg) ?: throw IllegalStateException("Unable to load audio file: $fileName") + + this.channels = channels + this.sampleRate = sampleRate + } + + /** + * Convert from a ShortBuffer generated by the STBVorbis call to a ShortBuffer that can be garbage collected + */ + private fun convertToGCBuffer(ogg: ShortBuffer?): ShortBuffer? { + return if (ogg != null) { + val buffer = BufferUtils.createShortBuffer(ogg.remaining()) + buffer.put(ogg).position(0) + freeVorbis(ogg) + buffer + } else null + } + + private fun freeVorbis(buffer: ShortBuffer) { + buffer.position(0) + LibCStdlib.free(buffer) + } +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/config/BackendDebugConfiguration.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/config/BackendDebugConfiguration.kt index 2326c6db..e25130a8 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/config/BackendDebugConfiguration.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/config/BackendDebugConfiguration.kt @@ -19,4 +19,12 @@ object BackendDebugConfiguration : DebugConfiguration { get() = configuration?.printWarnings ?: false override val lwjglDebug: Boolean get() = configuration?.lwjglDebug ?: false + + override fun pauseUpdates(pause: Boolean) { + configuration?.pauseUpdates(pause) + } + + override fun frameAdvance() { + configuration?.frameAdvance() + } } \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/config/ConfigUtils.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/config/ConfigUtils.kt new file mode 100644 index 00000000..ca301d19 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/config/ConfigUtils.kt @@ -0,0 +1,8 @@ +package com.mechanica.engine.config + +import com.mechanica.engine.configuration.Configurable +import com.mechanica.engine.context.DesktopApplication + +fun Configurable.configure(setup: T.() -> Unit) { + this.configureAs(DesktopApplication(), setup) +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/context/ALContext.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/context/ALContext.kt new file mode 100644 index 00000000..1fbd2308 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/context/ALContext.kt @@ -0,0 +1,35 @@ +package com.mechanica.engine.context + +import org.lwjgl.openal.AL +import org.lwjgl.openal.ALC +import org.lwjgl.openal.ALC10.* +import java.nio.ByteBuffer +import java.nio.IntBuffer + +object ALContext { + val device by lazy { getDefaultDevice() } + val context by lazy { createContext() } + + fun initialize() { + val capabilities = ALC.createCapabilities(device) + + alcMakeContextCurrent(context) + + AL.createCapabilities(capabilities) + } + + fun destroy() { + alcDestroyContext(context) + alcCloseDevice(device) + } + + private fun getDefaultDevice(): Long { + val specifier: ByteBuffer? = null + return alcOpenDevice(specifier) + } + + private fun createContext(): Long { + val attrs: IntBuffer? = null + return alcCreateContext(device, attrs) + } +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/context/DesktopApplication.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/context/DesktopApplication.kt new file mode 100644 index 00000000..b48c782f --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/context/DesktopApplication.kt @@ -0,0 +1,23 @@ +package com.mechanica.engine.context + +import com.mechanica.engine.context.loader.LwjglLoader +import com.mechanica.engine.display.Window + +class DesktopApplication : Application { + override fun initialize(window: Window) { + GLContext.initialize(window) + val callbacks = GLInitializer.initialize(LwjglLoader()) + GLContext.setCallbacks(window, callbacks) + ALContext.initialize() + } + + override fun terminate() { + GLContext.free() + GLFWContext.terminate() + ALContext.destroy() + } + + override fun startFrame() { + GLContext.startFrame() + } +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/context/GLContext.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/context/GLContext.kt index 86533d47..a13dd4ed 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/context/GLContext.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/context/GLContext.kt @@ -1,6 +1,8 @@ package com.mechanica.engine.context +import com.mechanica.engine.context.callbacks.EventCallbacks import com.mechanica.engine.display.Window +import com.mechanica.engine.input.* import com.mechanica.engine.utils.enableAlphaBlending import org.lwjgl.glfw.GLFW import org.lwjgl.opengl.* @@ -20,6 +22,9 @@ object GLContext : Version { override val version: Double get() = parsedVersionString?.version ?: throw IllegalStateException(errorMessage) + var initialized = false + private set + fun isExtensionSupported(extension: String): Boolean { return extensionMap.containsKey(extension) } @@ -45,6 +50,7 @@ object GLContext : Version { GL11.glEnable(GL11.GL_STENCIL_TEST) enableAlphaBlending() + initialized = true } private fun initContext(windowId: Long) { @@ -52,6 +58,17 @@ object GLContext : Version { GL.createCapabilities() } + + fun setCallbacks(window: Window, callbacks: EventCallbacks) { + GLFW.glfwSetKeyCallback(window.id, GLFWKeyHandler(callbacks.keyboardHandler)) + GLFW.glfwSetCharCallback(window.id, GLFWTextInputHandler(callbacks.keyboardHandler)) + + GLFW.glfwSetMouseButtonCallback(window.id, GLFWMouseButtonHandler(callbacks.mouseHandler)) + GLFW.glfwSetCursorPosCallback(window.id, GLFWMouseCursorHandler(callbacks.mouseHandler)) + GLFW.glfwSetScrollCallback(window.id, GLFWScrollHandler(callbacks.mouseHandler)) + + } + private fun parseVersionString(): GLVersionStringParser { val versionString: String = GL11.glGetString(GL11.GL_VERSION) ?: throw IllegalStateException("Unable to get the version of OpenGL") diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/context/GLFWContext.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/context/GLFWContext.kt index 59a72dc3..329d3e3a 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/context/GLFWContext.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/context/GLFWContext.kt @@ -8,7 +8,8 @@ object GLFWContext : Version { override val minorVersion: Int override val version: Double - private var initialized = false + var initialized = false + private set private var _samples = 4 val samples: Int diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglAudioLoader.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglAudioLoader.kt new file mode 100644 index 00000000..b8584091 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglAudioLoader.kt @@ -0,0 +1,11 @@ +package com.mechanica.engine.context.loader + +import com.mechanica.engine.audio.* + +class LwjglAudioLoader : AudioLoader { + override fun sound(file: String) = ALSound(file) + + override fun source(sound: Sound) = ALSource(sound) + + override fun listener() = ALListener() +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglBufferLoader.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglBufferLoader.kt index 90b81eee..d399e01e 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglBufferLoader.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglBufferLoader.kt @@ -1,6 +1,6 @@ package com.mechanica.engine.context.loader -import com.mechanica.engine.context.loader.BufferLoader +import com.mechanica.engine.audio.OggAudioFile import org.lwjgl.BufferUtils import org.lwjgl.system.MemoryUtil import java.nio.ByteBuffer diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglDisplayLoader.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglDisplayLoader.kt new file mode 100644 index 00000000..2dc116f4 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglDisplayLoader.kt @@ -0,0 +1,27 @@ +package com.mechanica.engine.context.loader + +import com.mechanica.engine.display.GLFWMonitor +import com.mechanica.engine.display.GLFWWindow +import com.mechanica.engine.display.Monitor +import com.mechanica.engine.display.Window + +class LwjglDisplayLoader : DisplayLoader { + override fun createWindow(title: String, width: Int, height: Int): Window { + return GLFWWindow.create(title, width, height) + } + + override fun createWindow(title: String, monitor: Monitor): Window { + return GLFWWindow.create(title, monitor) + } + + override fun createWindow(title: String, width: Int, height: Int, monitor: Monitor): Window { + return GLFWWindow.create(title, width, height, monitor) + } + + override val allMonitors: Array + get() = GLFWMonitor.allMonitors + + override fun getPrimaryMonitor(): Monitor { + return GLFWMonitor.getPrimaryMonitor() + } +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglFontLoader.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglFontLoader.kt index 059d9898..1a6fca54 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglFontLoader.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglFontLoader.kt @@ -1,12 +1,16 @@ package com.mechanica.engine.context.loader -import com.mechanica.engine.text.LwjglFont +import com.mechanica.engine.text.LwjglStandardFont import com.mechanica.engine.text.Font -import com.mechanica.engine.context.loader.FontLoader import com.mechanica.engine.resources.Resource +import com.mechanica.engine.text.FontAtlasConfiguration class LwjglFontLoader : FontLoader { - override val defaultFont: Font by lazy { LwjglFont(Resource("res/fonts/Roboto-Regular.ttf")) } + override val defaultFont: Font by lazy { LwjglStandardFont(Resource("res/fonts/Roboto-Regular.ttf")) { + width = 1024 + height = 1024 + characterSize = 200f + } } - override fun font(res: Resource) = LwjglFont(res) + override fun font(res: Resource, initializer: FontAtlasConfiguration.() -> Unit) = LwjglStandardFont(res, initializer) } diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglInputLoader.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglInputLoader.kt new file mode 100644 index 00000000..3d0cf566 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglInputLoader.kt @@ -0,0 +1,8 @@ +package com.mechanica.engine.context.loader + +import com.mechanica.engine.input.GLFWKeyIDs +import com.mechanica.engine.input.KeyIDs + +class LwjglInputLoader : InputLoader { + override fun keyIds() = GLFWKeyIDs() +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglLoader.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglLoader.kt index 25cb78c5..3b1a3939 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglLoader.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglLoader.kt @@ -1,18 +1,44 @@ package com.mechanica.engine.context.loader +import com.mechanica.engine.display.GLFWWindow import com.mechanica.engine.shader.qualifiers.AttributeQualifier import com.mechanica.engine.shader.qualifiers.Qualifier +import com.mechanica.engine.shader.script.Shader +import com.mechanica.engine.shader.script.ShaderLoader +import com.mechanica.engine.shader.script.ShaderScript import com.mechanica.engine.shader.vbo.LwjglElementArrayType +import org.lwjgl.opengl.GL20 class LwjglLoader : GLLoader { override val constants = LwjglConstants() override val bufferLoader = LwjglBufferLoader() override val fontLoader = LwjglFontLoader() override val graphicsLoader = LwjglGraphicsLoader() + override val audioLoader = LwjglAudioLoader() + override val inputLoader = LwjglInputLoader() override fun createAttributeLoader(qualifier: AttributeQualifier) = LwjglAttributeLoader(qualifier) override fun createUniformLoader(qualifier: Qualifier) = LwjglUniformLoader(qualifier) override fun createElementArray() = LwjglElementArrayType() + + override fun defaultShader(vertex: ShaderScript, fragment: ShaderScript, tessellation: ShaderScript?, geometry: ShaderScript?): Shader { + return object : Shader() { + override val vertex = vertex + override val fragment = fragment + override val tessellation = tessellation + override val geometry = geometry + + private val loader: ShaderLoader by lazy { ShaderLoader(vertex, fragment, tessellation, geometry) } + + override val id: Int get() = loader.id + + override fun loadProgram(id: Int) { + GL20.glUseProgram(id) + } + + override fun loadUniformLocation(name: String) = GL20.glGetUniformLocation(id, name) + } + } } \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglUniformLoader.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglUniformLoader.kt index 5736d15e..0b0c7669 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglUniformLoader.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/context/loader/LwjglUniformLoader.kt @@ -1,8 +1,7 @@ package com.mechanica.engine.context.loader -import com.mechanica.engine.context.loader.UniformLoader import com.mechanica.engine.shader.qualifiers.Qualifier -import com.mechanica.engine.shader.vars.uniforms.* +import com.mechanica.engine.shader.uniforms.* import org.joml.Matrix4f class LwjglUniformLoader(override val qualifier: Qualifier): UniformLoader { diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/display/Monitor.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/display/GLFWMonitor.kt similarity index 79% rename from desktop-application/src/main/kotlin/com/mechanica/engine/display/Monitor.kt rename to desktop-application/src/main/kotlin/com/mechanica/engine/display/GLFWMonitor.kt index dd0a3365..71270e5f 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/display/Monitor.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/display/GLFWMonitor.kt @@ -7,9 +7,14 @@ import org.lwjgl.glfw.GLFWMonitorCallback import org.lwjgl.glfw.GLFWVidMode import org.lwjgl.system.MemoryStack -class Monitor private constructor(val id: Long) { +class GLFWMonitor private constructor(override val id: Long) : Monitor { - val name by lazy { glfwGetMonitorName(id) ?: "N/A" } + override val name by lazy { glfwGetMonitorName(id) ?: "N/A" } + + override val width: Int + get() = currentVideoMode.width() + override val height: Int + get() = currentVideoMode.height() var monitorUserPointer: Long get() = glfwGetMonitorUserPointer(id) @@ -19,6 +24,7 @@ class Monitor private constructor(val id: Long) { val currentVideoMode: GLFWVidMode get() = glfwGetVideoMode(id) ?: throw IllegalStateException("Error retrieving the current Vid Mode") + val supportedVideoModes: Array get() { val vidmodes = glfwGetVideoModes(id) @@ -29,7 +35,7 @@ class Monitor private constructor(val id: Long) { } } - val size: Size + override val size: Monitor.Size get() { var width = -1 var height = -1 @@ -40,10 +46,10 @@ class Monitor private constructor(val id: Long) { width = widthBuffer[0] height = heightBuffer[0] } - return Size(width, height) + return Monitor.Size(width, height) } - val contentScale: ContentScale + override val contentScale: Monitor.ContentScale get() { var xScale = 1f var yScale = 1f @@ -54,7 +60,7 @@ class Monitor private constructor(val id: Long) { xScale = xBuffer[0] yScale = yBuffer[0] } - return ContentScale(xScale, yScale) + return Monitor.ContentScale(xScale, yScale) } var gammaRamp: GLFWGammaRamp @@ -71,10 +77,6 @@ class Monitor private constructor(val id: Long) { field = value } - data class Size(val width_mm: Int, val height_mm: Int) - - data class ContentScale(val xScale: Float, val yScale: Float) - companion object { var allMonitors = createMonitorsArray() private set @@ -83,22 +85,22 @@ class Monitor private constructor(val id: Long) { return field } - fun getPrimaryMonitor(): Monitor { + fun getPrimaryMonitor(): GLFWMonitor { GLFWContext.initialize() - return Monitor(glfwGetPrimaryMonitor()) + return GLFWMonitor(glfwGetPrimaryMonitor()) } - private fun createMonitorsArray(): Array { + private fun createMonitorsArray(): Array { GLFWContext.initialize() val pointers = glfwGetMonitors() return if (pointers != null) { - Array(pointers.limit()) {Monitor(pointers[it])} + Array(pointers.limit()) {GLFWMonitor(pointers[it])} } else { emptyArray() } } - private fun checkMonitors(monitors: Array): Array { + private fun checkMonitors(monitors: Array): Array { val pointers = glfwGetMonitors() if (pointers != null) { val pointerSize = pointers.limit() diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/display/Window.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/display/GLFWWindow.kt similarity index 66% rename from desktop-application/src/main/kotlin/com/mechanica/engine/display/Window.kt rename to desktop-application/src/main/kotlin/com/mechanica/engine/display/GLFWWindow.kt index 40f89506..0ac38f20 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/display/Window.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/display/GLFWWindow.kt @@ -1,69 +1,75 @@ package com.mechanica.engine.display +import com.mechanica.engine.context.GLContext import com.mechanica.engine.context.GLFWContext +import com.mechanica.engine.context.callbacks.EventCallbacks +import com.mechanica.engine.resources.Resource import com.mechanica.engine.utils.ImageData import org.lwjgl.glfw.Callbacks import org.lwjgl.glfw.GLFW.* import org.lwjgl.glfw.GLFWImage import org.lwjgl.system.MemoryUtil -import com.mechanica.engine.resources.Resource import java.nio.ByteBuffer -class Window private constructor(width: Int, height: Int, val title: String, monitor: Monitor?) { - val id: Long= glfwCreateWindow(width, height, title, monitor?.id ?: MemoryUtil.NULL, MemoryUtil.NULL) +class GLFWWindow private constructor(width: Int, height: Int, override val title: String, monitor: Monitor?) : Window { + override val id: Long= glfwCreateWindow(width, height, title, monitor?.id ?: MemoryUtil.NULL, MemoryUtil.NULL) var hasInitialized = false private set - val width: Int + override val width: Int get() = resolution.width - val height: Int + override val height: Int get() = resolution.height - val aspectRatio: Double + override val aspectRatio: Double get() = width.toDouble()/height.toDouble() - var isFocused: Boolean + override var isFocused: Boolean get() = glfwGetWindowAttrib(id, GLFW_FOCUSED) == GLFW_TRUE set(value) { if (value) glfwFocusWindow(id) } - var isIconified: Boolean + override var isIconified: Boolean get() = glfwGetWindowAttrib(id, GLFW_ICONIFIED ) == GLFW_TRUE set(value) { if (value) glfwIconifyWindow(id) else glfwRestoreWindow(id) } - var isMaximized: Boolean + override var isMaximized: Boolean get() = glfwGetWindowAttrib(id, GLFW_MAXIMIZED ) == GLFW_TRUE set(value) { if (value) glfwMaximizeWindow(id) else glfwRestoreWindow(id) } - val isHovered: Boolean + override val isHovered: Boolean get() = glfwGetWindowAttrib(id, GLFW_HOVERED ) == GLFW_TRUE - var isVisible: Boolean + override var isVisible: Boolean get() = glfwGetWindowAttrib(id, GLFW_VISIBLE ) == GLFW_TRUE set(value) { if (value) glfwShowWindow(id) else glfwHideWindow(id) } - var isResizable: Boolean + override var isResizable: Boolean get() = glfwGetWindowAttrib(id, GLFW_RESIZABLE ) == 1 set(value) { glfwSetWindowAttrib(id, GLFW_RESIZABLE, if (value) GLFW_TRUE else GLFW_FALSE ) } - var isDecorated: Boolean + override var isDecorated: Boolean get() = glfwGetWindowAttrib(id, GLFW_DECORATED ) == 1 set(value) { glfwSetWindowAttrib(id, GLFW_DECORATED, if (value) GLFW_TRUE else GLFW_FALSE ) } - var isFloating: Boolean + override var isFloating: Boolean get() = glfwGetWindowAttrib(id, GLFW_FLOATING ) == 1 set(value) { glfwSetWindowAttrib(id, GLFW_FLOATING, if (value) GLFW_TRUE else GLFW_FALSE ) } - var shouldClose: Boolean + override var shouldClose: Boolean get() = glfwWindowShouldClose(id) set(value) { glfwSetWindowShouldClose(id, value) } - var vSync: Boolean = true + override var vSync: Boolean = true set(value) { - glfwSwapInterval(if (value) 1 else 0) + if (GLContext.initialized) { + glfwSwapInterval(if (value) 1 else 0) + } field = value } + override val isFullscreen: Boolean + get() = monitor != null private var finished = false - var opacity: Float + override var opacity: Float get() = glfwGetWindowOpacity(id) set(value) = glfwSetWindowOpacity(id, value) @@ -71,7 +77,7 @@ class Window private constructor(width: Int, height: Int, val title: String, mon get() { val monitor = field val monitorId = glfwGetWindowMonitor(id) - val foundMonitor = Monitor.allMonitors.firstOrNull { it.id == monitorId } + val foundMonitor = GLFWMonitor.allMonitors.firstOrNull { it.id == monitorId } return if (monitorId == MemoryUtil.NULL){ field = null null @@ -88,17 +94,17 @@ class Window private constructor(width: Int, height: Int, val title: String, mon field = value } - val position: Position = Position() + override val position: PositionImpl = PositionImpl() - val resolution: Dimension by lazy { + override val resolution: Window.Dimension by lazy { setResolution(DimensionImpl(0, 0, false)) } - val size: Dimension by lazy { + override val size: Window.Dimension by lazy { setWindowSize(DimensionImpl(0, 0, false)) } - val isResizing: Boolean + override val isResizing: Boolean get() { val resolution = (resolution as DimensionImpl) val size = (size as DimensionImpl) @@ -130,7 +136,7 @@ class Window private constructor(width: Int, height: Int, val title: String, mon } - fun addRefreshCallback(callback: (Window) -> Unit) { + override fun addRefreshCallback(callback: (Window) -> Unit) { callbackList.add(callback) } @@ -157,8 +163,9 @@ class Window private constructor(width: Int, height: Int, val title: String, mon } } - fun update(): Boolean { + override fun update(): Boolean { swapBuffers() + EventCallbacks.prepare() pollEvents() val shouldClose = this.shouldClose if (finished) { @@ -167,7 +174,7 @@ class Window private constructor(width: Int, height: Int, val title: String, mon return !shouldClose } - fun destroy() { + override fun destroy() { if (hasInitialized) { Callbacks.glfwFreeCallbacks(id) glfwDestroyWindow(id) @@ -183,11 +190,11 @@ class Window private constructor(width: Int, height: Int, val title: String, mon glfwPollEvents() } - fun requestAttention() { + override fun requestAttention() { glfwRequestWindowAttention(id) } - private fun setWindowSize(out: DimensionImpl): Dimension { + private fun setWindowSize(out: DimensionImpl): Window.Dimension { val widthArray = IntArray(1) val heightArray = IntArray(1) glfwGetWindowSize(id, widthArray, heightArray) @@ -196,7 +203,7 @@ class Window private constructor(width: Int, height: Int, val title: String, mon return out } - private fun setResolution(out: DimensionImpl): Dimension { + private fun setResolution(out: DimensionImpl): Window.Dimension { val widthArray = IntArray(1) val heightArray = IntArray(1) glfwGetFramebufferSize(id, widthArray, heightArray) @@ -205,7 +212,7 @@ class Window private constructor(width: Int, height: Int, val title: String, mon return out } - fun setIcon(resource: Resource) { + override fun setIcon(resource: Resource) { val image = ImageData(resource) if (image.data != null) { @@ -215,7 +222,7 @@ class Window private constructor(width: Int, height: Int, val title: String, mon image.free() } - fun setIcon(width: Int, height: Int, imageBuffer: ByteBuffer) { + override fun setIcon(width: Int, height: Int, imageBuffer: ByteBuffer) { val image: GLFWImage = GLFWImage.malloc() val imagebf: GLFWImage.Buffer = GLFWImage.malloc(1) @@ -225,32 +232,55 @@ class Window private constructor(width: Int, height: Int, val title: String, mon glfwSetWindowIcon(id, imagebf) } - fun setFullscreen(monitor: Monitor) { - val vidMode = monitor.currentVideoMode + override fun setFullscreen() { + setFullscreen(Monitor.getPrimaryMonitor()) + } + + override fun setFullscreen(monitor: Monitor) { + val vidMode = if (monitor is GLFWMonitor) monitor.currentVideoMode else throw IllegalStateException("Unable to put window into fullscreen mode") glfwSetWindowMonitor(id, monitor.id, 0, 0, vidMode.width(), vidMode.height(), vidMode.refreshRate()) } - fun setFullscreen(monitor: Monitor, width: Int, height: Int, refreshRate: Int = 60) { + override fun setFullscreen(monitor: Monitor, width: Int, height: Int, refreshRate: Int) { glfwSetWindowMonitor(id, monitor.id, 0, 0, width, height, refreshRate) } - fun exitFullscreen() { + override fun exitFullscreen(width: Int, height: Int) { if (this.monitor != null) { - val vidMode = Monitor.getPrimaryMonitor().currentVideoMode - glfwSetWindowMonitor(id, MemoryUtil.NULL, 0, 0, vidMode.width(), vidMode.height(), 0) + val vidMode = GLFWMonitor.getPrimaryMonitor().currentVideoMode + val screenWidth = vidMode.width() + val screenHeight = vidMode.height() + + if (width < 0 || height < 0) { + setDefaultWindowDimensions(screenWidth, screenHeight) + } else { + val x = screenWidth - width/2 + val y = screenHeight - height/2 + glfwSetWindowMonitor(id, MemoryUtil.NULL, x, y, width, height, vidMode.refreshRate()) + + } } } - inner class Position { + private fun setDefaultWindowDimensions(screenWidth: Int, screenHeight: Int) { + val windowWidth = (0.75*screenWidth).toInt() + val windowHeight = (0.75*screenHeight).toInt() + val x = screenWidth/2 - windowWidth/2 + val y = screenHeight/2 - windowHeight/2 + glfwSetWindowMonitor(id, MemoryUtil.NULL, x, y, windowWidth, windowHeight, 0) + + } + + inner class PositionImpl : Window.Position { val xArray = IntArray(1) val yArray = IntArray(1) - var x: Int + override var x: Int set(value) = set(value, y) get() { updatePosition() return xArray[0] } - var y: Int + override var y: Int set(value) = set(x, value) get() { updatePosition() @@ -261,29 +291,25 @@ class Window private constructor(width: Int, height: Int, val title: String, mon glfwGetWindowPos(id, xArray, yArray) } - fun set(x: Int, y: Int) { + override fun set(x: Int, y: Int) { glfwSetWindowPos(id, x, y) } } - interface Dimension { - val width: Int - val height: Int - } private data class DimensionImpl( override var width: Int, override var height: Int, - var isChanging: Boolean) : Dimension + var isChanging: Boolean) : Window.Dimension companion object { fun create(title: String, width: Int, height: Int): Window { GLFWContext.initialize() - return Window(width, height, title, null) + return GLFWWindow(width, height, title, null) } fun create(title: String, monitor: Monitor): Window { GLFWContext.initialize() - val vidMode = monitor.currentVideoMode + val vidMode = if (monitor is GLFWMonitor) monitor.currentVideoMode else throw IllegalStateException("Unable to create window") val width = vidMode.width() val height = vidMode.height() glfwWindowHint(GLFW_RED_BITS, vidMode.redBits()) @@ -291,12 +317,12 @@ class Window private constructor(width: Int, height: Int, val title: String, mon glfwWindowHint(GLFW_BLUE_BITS, vidMode.blueBits()) glfwWindowHint(GLFW_REFRESH_RATE, vidMode.refreshRate()) - return Window(width, height, title, monitor) + return GLFWWindow(width, height, title, monitor) } fun create(title: String, width: Int, height: Int, monitor: Monitor): Window { GLFWContext.initialize() - return Window(width, height, title, monitor) + return GLFWWindow(width, height, title, monitor) } } diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWKeyHandler.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWKeyHandler.kt new file mode 100644 index 00000000..312dd561 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWKeyHandler.kt @@ -0,0 +1,15 @@ +package com.mechanica.engine.input + +import com.mechanica.engine.context.callbacks.KeyboardHandler +import org.lwjgl.glfw.GLFW +import org.lwjgl.glfw.GLFWKeyCallback + +class GLFWKeyHandler(private val handler: KeyboardHandler) : GLFWKeyCallback() { + override fun invoke(window: Long, key: Int, scancode: Int, action: Int, mods: Int) { + if (action == GLFW.GLFW_PRESS) { + handler.keyPressed(key) + } else if (action == GLFW.GLFW_RELEASE) { + handler.keyReleased(key) + } + } +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWKeyIDs.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWKeyIDs.kt new file mode 100644 index 00000000..ad1d53d1 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWKeyIDs.kt @@ -0,0 +1,148 @@ +package com.mechanica.engine.input + +import org.lwjgl.glfw.GLFW.* + +class GLFWKeyIDs : KeyIDs { + override val UNKNOWN = GLFWKey(GLFW_KEY_UNKNOWN) + override val M1 = GLFWKey(GLFW_MOUSE_BUTTON_1, "LEFT CLICK") + override val M2 = GLFWKey(GLFW_MOUSE_BUTTON_2, "RIGHT CLICK") + override val M3 = GLFWKey(GLFW_MOUSE_BUTTON_3) + override val M4 = GLFWKey(GLFW_MOUSE_BUTTON_4) + override val M5 = GLFWKey(GLFW_MOUSE_BUTTON_5) + override val M6 = GLFWKey(GLFW_MOUSE_BUTTON_6) + override val M7 = GLFWKey(GLFW_MOUSE_BUTTON_7) + override val M8 = GLFWKey(GLFW_MOUSE_BUTTON_8) + override val SPACE = GLFWKey(GLFW_KEY_SPACE, "SPACE") + override val APOSTROPHE = GLFWKey(GLFW_KEY_APOSTROPHE) + override val COMMA = GLFWKey(GLFW_KEY_COMMA) + override val MINUS = GLFWKey(GLFW_KEY_MINUS) + override val PERIOD = GLFWKey(GLFW_KEY_PERIOD) + override val SLASH = GLFWKey(GLFW_KEY_SLASH) + override val N0 = GLFWKey(GLFW_KEY_0) + override val N1 = GLFWKey(GLFW_KEY_1) + override val N2 = GLFWKey(GLFW_KEY_2) + override val N3 = GLFWKey(GLFW_KEY_3) + override val N4 = GLFWKey(GLFW_KEY_4) + override val N5 = GLFWKey(GLFW_KEY_5) + override val N6 = GLFWKey(GLFW_KEY_6) + override val N7 = GLFWKey(GLFW_KEY_7) + override val N8 = GLFWKey(GLFW_KEY_8) + override val N9 = GLFWKey(GLFW_KEY_9) + override val SEMICOLON = GLFWKey(GLFW_KEY_SEMICOLON) + override val EQUAL = GLFWKey(GLFW_KEY_EQUAL) + override val A = GLFWKey(GLFW_KEY_A) + override val B = GLFWKey(GLFW_KEY_B) + override val C = GLFWKey(GLFW_KEY_C) + override val D = GLFWKey(GLFW_KEY_D) + override val E = GLFWKey(GLFW_KEY_E) + override val F = GLFWKey(GLFW_KEY_F) + override val G = GLFWKey(GLFW_KEY_G) + override val H = GLFWKey(GLFW_KEY_H) + override val I = GLFWKey(GLFW_KEY_I) + override val J = GLFWKey(GLFW_KEY_J) + override val K = GLFWKey(GLFW_KEY_K) + override val L = GLFWKey(GLFW_KEY_L) + override val M = GLFWKey(GLFW_KEY_M) + override val N = GLFWKey(GLFW_KEY_N) + override val O = GLFWKey(GLFW_KEY_O) + override val P = GLFWKey(GLFW_KEY_P) + override val Q = GLFWKey(GLFW_KEY_Q) + override val R = GLFWKey(GLFW_KEY_R) + override val S = GLFWKey(GLFW_KEY_S) + override val T = GLFWKey(GLFW_KEY_T) + override val U = GLFWKey(GLFW_KEY_U) + override val V = GLFWKey(GLFW_KEY_V) + override val W = GLFWKey(GLFW_KEY_W) + override val X = GLFWKey(GLFW_KEY_X) + override val Y = GLFWKey(GLFW_KEY_Y) + override val Z = GLFWKey(GLFW_KEY_Z) + override val LEFT_BRACKET = GLFWKey(GLFW_KEY_LEFT_BRACKET) + override val BACKSLASH = GLFWKey(GLFW_KEY_BACKSLASH) + override val RIGHT_BRACKET = GLFWKey(GLFW_KEY_RIGHT_BRACKET) + override val GRAVE_ACCENT = GLFWKey(GLFW_KEY_GRAVE_ACCENT) + override val WORLD_1 = GLFWKey(GLFW_KEY_WORLD_1) + override val WORLD_2 = GLFWKey(GLFW_KEY_WORLD_2) + override val ESC = GLFWKey(GLFW_KEY_ESCAPE) + override val ENTER = GLFWKey(GLFW_KEY_ENTER) + override val TAB = GLFWKey(GLFW_KEY_TAB) + override val BACKSPACE = GLFWKey(GLFW_KEY_BACKSPACE) + override val INSERT = GLFWKey(GLFW_KEY_INSERT) + override val DELETE = GLFWKey(GLFW_KEY_DELETE) + override val RIGHT = GLFWKey(GLFW_KEY_RIGHT) + override val LEFT = GLFWKey(GLFW_KEY_LEFT) + override val DOWN = GLFWKey(GLFW_KEY_DOWN) + override val UP = GLFWKey(GLFW_KEY_UP) + override val PAGE_UP = GLFWKey(GLFW_KEY_PAGE_UP) + override val PAGE_DOWN = GLFWKey(GLFW_KEY_PAGE_DOWN) + override val HOME = GLFWKey(GLFW_KEY_HOME) + override val END = GLFWKey(GLFW_KEY_END) + override val CAPS_LOCK = GLFWKey(GLFW_KEY_CAPS_LOCK) + override val SCROLL_LOCK = GLFWKey(GLFW_KEY_SCROLL_LOCK) + override val NUM_LOCK = GLFWKey(GLFW_KEY_NUM_LOCK) + override val PRINT_SCREEN = GLFWKey(GLFW_KEY_PRINT_SCREEN) + override val PAUSE = GLFWKey(GLFW_KEY_PAUSE) + override val F1 = GLFWKey(GLFW_KEY_F1) + override val F2 = GLFWKey(GLFW_KEY_F2) + override val F3 = GLFWKey(GLFW_KEY_F3) + override val F4 = GLFWKey(GLFW_KEY_F4) + override val F5 = GLFWKey(GLFW_KEY_F5) + override val F6 = GLFWKey(GLFW_KEY_F6) + override val F7 = GLFWKey(GLFW_KEY_F7) + override val F8 = GLFWKey(GLFW_KEY_F8) + override val F9 = GLFWKey(GLFW_KEY_F9) + override val F10 = GLFWKey(GLFW_KEY_F10) + override val F11 = GLFWKey(GLFW_KEY_F11) + override val F12 = GLFWKey(GLFW_KEY_F12) + override val F13 = GLFWKey(GLFW_KEY_F13) + override val F14 = GLFWKey(GLFW_KEY_F14) + override val F15 = GLFWKey(GLFW_KEY_F15) + override val F16 = GLFWKey(GLFW_KEY_F16) + override val F17 = GLFWKey(GLFW_KEY_F17) + override val F18 = GLFWKey(GLFW_KEY_F18) + override val F19 = GLFWKey(GLFW_KEY_F19) + override val F20 = GLFWKey(GLFW_KEY_F20) + override val F21 = GLFWKey(GLFW_KEY_F21) + override val F22 = GLFWKey(GLFW_KEY_F22) + override val F23 = GLFWKey(GLFW_KEY_F23) + override val F24 = GLFWKey(GLFW_KEY_F24) + override val F25 = GLFWKey(GLFW_KEY_F25) + override val KP_0 = GLFWKey(GLFW_KEY_KP_0) + override val KP_1 = GLFWKey(GLFW_KEY_KP_1) + override val KP_2 = GLFWKey(GLFW_KEY_KP_2) + override val KP_3 = GLFWKey(GLFW_KEY_KP_3) + override val KP_4 = GLFWKey(GLFW_KEY_KP_4) + override val KP_5 = GLFWKey(GLFW_KEY_KP_5) + override val KP_6 = GLFWKey(GLFW_KEY_KP_6) + override val KP_7 = GLFWKey(GLFW_KEY_KP_7) + override val KP_8 = GLFWKey(GLFW_KEY_KP_8) + override val KP_9 = GLFWKey(GLFW_KEY_KP_9) + override val KP_DECIMAL = GLFWKey(GLFW_KEY_KP_DECIMAL) + override val KP_DIVIDE = GLFWKey(GLFW_KEY_KP_DIVIDE) + override val KP_MULTIPLY = GLFWKey(GLFW_KEY_KP_MULTIPLY) + override val KP_SUBTRACT = GLFWKey(GLFW_KEY_KP_SUBTRACT) + override val KP_ADD = GLFWKey(GLFW_KEY_KP_ADD) + override val KP_ENTER = GLFWKey(GLFW_KEY_KP_ENTER) + override val KP_EQUAL = GLFWKey(GLFW_KEY_KP_EQUAL) + override val LSHIFT = GLFWKey(GLFW_KEY_LEFT_SHIFT, "SHIFT") + override val LCTRL = GLFWKey(GLFW_KEY_LEFT_CONTROL) + override val LALT = GLFWKey(GLFW_KEY_LEFT_ALT) + override val LEFT_SUPER = GLFWKey(GLFW_KEY_LEFT_SUPER) + override val RSHIFT = GLFWKey(GLFW_KEY_RIGHT_SHIFT) + override val RCTRL = GLFWKey(GLFW_KEY_RIGHT_CONTROL) + override val RALT = GLFWKey(GLFW_KEY_RIGHT_ALT) + override val RIGHT_SUPER = GLFWKey(GLFW_KEY_RIGHT_SUPER) + override val MENU = GLFWKey(GLFW_KEY_MENU) + override val SCROLL_UP = GLFWKey(1000, "scroll up") + override val SCROLL_DOWN = GLFWKey(1001, "scroll down") + override val SCROLL = GLFWKey(1002, "scroll") + + class GLFWKey(override val id: Int, label: String? = null) : KeyID { + private var _label: String? = label + + override val label: String + get() { + val l = _label + return l ?: glfwGetKeyName(id, 0)?.also { _label = it } ?: "?" + } + } +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWMouseButtonHandler.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWMouseButtonHandler.kt new file mode 100644 index 00000000..84369d26 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWMouseButtonHandler.kt @@ -0,0 +1,15 @@ +package com.mechanica.engine.input + +import com.mechanica.engine.context.callbacks.MouseHandler +import org.lwjgl.glfw.GLFW +import org.lwjgl.glfw.GLFWMouseButtonCallback + +class GLFWMouseButtonHandler(private val handler: MouseHandler) : GLFWMouseButtonCallback() { + override fun invoke(window: Long, button: Int, action: Int, mods: Int) { + if (action == GLFW.GLFW_PRESS) { + handler.buttonPressed(button) + } else if (action == GLFW.GLFW_RELEASE) { + handler.buttonReleased(button) + } + } +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWMouseCursorHandler.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWMouseCursorHandler.kt new file mode 100644 index 00000000..d3f5e3ed --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWMouseCursorHandler.kt @@ -0,0 +1,10 @@ +package com.mechanica.engine.input + +import com.mechanica.engine.context.callbacks.MouseHandler +import org.lwjgl.glfw.GLFWCursorPosCallback + +class GLFWMouseCursorHandler(private val handler: MouseHandler) : GLFWCursorPosCallback() { + override fun invoke(window: Long, x: Double, y: Double) { + handler.cursorMoved(x, y) + } +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWScrollHandler.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWScrollHandler.kt new file mode 100644 index 00000000..96ec4bd5 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWScrollHandler.kt @@ -0,0 +1,10 @@ +package com.mechanica.engine.input + +import com.mechanica.engine.context.callbacks.MouseHandler +import org.lwjgl.glfw.GLFWScrollCallback + +class GLFWScrollHandler(private val handler: MouseHandler): GLFWScrollCallback() { + override fun invoke(window: Long, xoffset: Double, yoffset: Double) { + handler.scroll(xoffset, yoffset) + } +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWTextInputHandler.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWTextInputHandler.kt new file mode 100644 index 00000000..4d917bf0 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/input/GLFWTextInputHandler.kt @@ -0,0 +1,11 @@ +package com.mechanica.engine.input + +import com.mechanica.engine.context.callbacks.KeyboardHandler +import org.lwjgl.glfw.GLFWCharCallback + + +class GLFWTextInputHandler(private val handler: KeyboardHandler) : GLFWCharCallback() { + override fun invoke(window: Long, codepoint: Int) { + handler.textInput(codepoint) + } +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/memory/MemoryTools.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/memory/MemoryTools.kt index 4887fd09..ad4e56ae 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/memory/MemoryTools.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/memory/MemoryTools.kt @@ -4,4 +4,10 @@ import org.lwjgl.system.MemoryStack inline fun useMemoryStack(use: MemoryStack.() -> Unit) { MemoryStack.stackPush().use { use(it) } +} + + +fun Byte.toIntValue(): Int = when { + (this.toInt() < 0) -> 255 + this.toInt() + 1 + else -> this.toInt() } \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/memory/RectangleFloatBuffers.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/memory/RectangleFloatBuffers.kt new file mode 100644 index 00000000..b578af94 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/memory/RectangleFloatBuffers.kt @@ -0,0 +1,12 @@ +package com.mechanica.engine.memory + +import org.lwjgl.system.MemoryStack +import java.nio.FloatBuffer +import java.nio.IntBuffer + +class RectangleFloatBuffers(stack: MemoryStack) { + val x: FloatBuffer = stack.floats(0f) + val y: FloatBuffer = stack.floats(0f) + val width: FloatBuffer = stack.floats(0f) + val height: FloatBuffer = stack.floats(0f) +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/memory/RectangleIntBuffers.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/memory/RectangleIntBuffers.kt new file mode 100644 index 00000000..598e2947 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/memory/RectangleIntBuffers.kt @@ -0,0 +1,11 @@ +package com.mechanica.engine.memory + +import org.lwjgl.system.MemoryStack +import java.nio.IntBuffer + +class RectangleIntBuffers(stack: MemoryStack) { + val x: IntBuffer = stack.ints(0) + val y: IntBuffer = stack.ints(0) + val width: IntBuffer = stack.ints(0) + val height: IntBuffer = stack.ints(0) +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/memory/buffer/BufferTools.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/memory/buffer/BufferTools.kt index f915831a..df88fc5c 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/memory/buffer/BufferTools.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/memory/buffer/BufferTools.kt @@ -1,7 +1,9 @@ package com.mechanica.engine.memory.buffer +import com.mechanica.engine.memory.BitmapBuffer import com.mechanica.engine.memory.useMemoryStack import org.lwjgl.BufferUtils +import org.lwjgl.stb.STBImageWrite import java.nio.FloatBuffer import java.nio.IntBuffer import java.nio.ShortBuffer @@ -78,4 +80,9 @@ fun FloatBuffer.contentsToString(): String { sb.append(this.get()).append(", ") } return sb.toString() +} + + +fun BitmapBuffer.write(name: String) { + STBImageWrite.stbi_write_png(name, width, height, 1, buffer, width) } \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/persistence/PersistenceUtil.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/persistence/PersistenceUtil.kt deleted file mode 100644 index 928224a9..00000000 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/persistence/PersistenceUtil.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.mechanica.engine.persistence - -import com.mechanica.engine.resources.ExternalResource -import java.io.File -import java.lang.reflect.Method -import java.util.* -import kotlin.collections.HashMap -import kotlin.collections.forEach -import kotlin.collections.set - -fun saveData(dataObj: Any) { - val getters = ArrayList() - for (method in dataObj.methods) { - if (method.name.startsWith("get") && method.name != "getClass") { - getters.add(method) - } - } - - val contentBuilder = StringBuilder() - - for (method in getters) { - contentBuilder - .append(method.camelCaseName).append(",") - .append(method.returnType.simpleName).append(",") - .append(method.invoke(dataObj)).append("\n") - } - val content = contentBuilder.toString() - - val file = resource(dataObj.dataFile, true) - file.write(content) - -} - -fun loadData(dataObj: Any) { - data class Variable(val name: String, val type: String, val value: String, val setter: Method?, val getter: Method?) - fun Variable.set() { - if (this.setter != null) { - when (this.type) { - "int" -> setter.invoke(dataObj, value.toIntOrNull()?: getter?.invoke(dataObj)?: 0) - "double" -> setter.invoke(dataObj, value.toDoubleOrNull()?: getter?.invoke(dataObj)?: 0.0) - "float" -> setter.invoke(dataObj, value.toFloatOrNull()?: getter?.invoke(dataObj)?: 0f) - "long" -> setter.invoke(dataObj, value.toLongOrNull()?: getter?.invoke(dataObj)?: 0L) - "boolean" -> setter.invoke(dataObj, value.toBoolean()) - "String" -> setter.invoke(dataObj, value) - } - } - } - - val setters = getSetters(dataObj) - val getters = getGetters(dataObj) - - val dataFile = resource(dataObj.dataFile, true) - - - dataFile.lines.forEach { - val params = it.split(",") - val name = params[0] - val v = Variable(name, params[1], params[2], setters[name], getters[name]) - v.set() - } - -} - -private fun getGetters(dataObj: Any): HashMap { - val getters = HashMap() - for (method in dataObj.methods) { - if (method.name.startsWith("get") && method.name != "getClass") { - getters[method.camelCaseName] = method - } - } - - return getters -} - - -private fun getSetters(dataObj: Any): HashMap { - val setters = HashMap() - for (method in dataObj.methods) { - if (method.name.startsWith("set")) { - setters[method.camelCaseName] = method - } - } - - return setters -} - -private fun resource(path: String, createIfAbsent: Boolean = false): ExternalResource { - val prefix = "res/" - return ExternalResource("$prefix$path", createIfAbsent) -} - -private val Method.camelCaseName: String - get() = this.name - .replace("get", "") - .replace("set", "") - .replace(0..1) { it.toLowerCase() + this } - -private fun String.replace(range: IntRange, function: String.(String) -> String): String { - val rangeString = this.substring(range) - val removed = this.removeRange(range) - return removed.function(rangeString) -} - -private const val directory: String = "data" - -private val Any.methods get() = this::class.java.methods - -private val Any.dataFile get() = directory + sep + this.name + ".txt" - -private val Any.name get() = this::class.java.name - -private val sep get() = File.separator diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/script/ShaderImpl.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/shader/script/ShaderImpl.kt deleted file mode 100644 index 8ac411f7..00000000 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/script/ShaderImpl.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.mechanica.engine.shader.script - -internal class ShaderImpl( - override val vertex: ShaderScript, - override val fragment: ShaderScript, - override val tessellation: ShaderScript?, - override val geometry: ShaderScript?): Shader() { - - private val loader: ShaderLoader by lazy { ShaderLoader(vertex, fragment, tessellation, geometry) } - - override val id: Int - get() = loader.id - -} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/script/ShaderLoader.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/shader/script/ShaderLoader.kt index 92835d8c..13c31302 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/script/ShaderLoader.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/shader/script/ShaderLoader.kt @@ -47,10 +47,8 @@ class ShaderLoader( val log = GL20.glGetProgramInfoLog(programId, GL20.GL_INFO_LOG_LENGTH) val script = getShaderScriptFromLog(log, vertex, fragment, tessellation, geometry) - System.err.println(errorMessage(script)) - System.err.println(log) cleanUp() - exitProcess(-1) + throw IllegalStateException(errorMessage(script) + "\n" + log) } } @@ -60,9 +58,6 @@ class ShaderLoader( GL20.glValidateProgram(id) checkProgram(id, GL20.GL_VALIDATE_STATUS) { "Shader validating failed" } - vertex.loadProgram(id) - geometry?.loadProgram(id) - fragment.loadProgram(id) } private fun loadShader(script: String, type: Int): Int { diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/LwjglFloat.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/shader/uniforms/LwjglFloat.kt similarity index 72% rename from desktop-application/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/LwjglFloat.kt rename to desktop-application/src/main/kotlin/com/mechanica/engine/shader/uniforms/LwjglFloat.kt index e10f4c46..75a26110 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/LwjglFloat.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/shader/uniforms/LwjglFloat.kt @@ -1,7 +1,7 @@ -package com.mechanica.engine.shader.vars.uniforms +package com.mechanica.engine.shader.uniforms import com.mechanica.engine.shader.qualifiers.Qualifier -import com.mechanica.engine.shader.vars.uniforms.vars.UniformFloat +import com.mechanica.engine.shader.uniforms.vars.UniformFloat import org.lwjgl.opengl.GL20.glUniform1f class LwjglFloat( diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/LwjglMatrix4f.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/shader/uniforms/LwjglMatrix4f.kt similarity index 80% rename from desktop-application/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/LwjglMatrix4f.kt rename to desktop-application/src/main/kotlin/com/mechanica/engine/shader/uniforms/LwjglMatrix4f.kt index 58cb422a..a9920796 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/LwjglMatrix4f.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/shader/uniforms/LwjglMatrix4f.kt @@ -1,7 +1,7 @@ -package com.mechanica.engine.shader.vars.uniforms +package com.mechanica.engine.shader.uniforms import com.mechanica.engine.shader.qualifiers.Qualifier -import com.mechanica.engine.shader.vars.uniforms.vars.UniformMatrix4f +import com.mechanica.engine.shader.uniforms.vars.UniformMatrix4f import org.joml.Matrix4f import org.lwjgl.BufferUtils import org.lwjgl.opengl.GL20.glUniformMatrix4fv diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/LwjglVector2f.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/shader/uniforms/LwjglVector2f.kt similarity index 74% rename from desktop-application/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/LwjglVector2f.kt rename to desktop-application/src/main/kotlin/com/mechanica/engine/shader/uniforms/LwjglVector2f.kt index 9c24284c..45e154f0 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/LwjglVector2f.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/shader/uniforms/LwjglVector2f.kt @@ -1,7 +1,7 @@ -package com.mechanica.engine.shader.vars.uniforms +package com.mechanica.engine.shader.uniforms import com.mechanica.engine.shader.qualifiers.Qualifier -import com.mechanica.engine.shader.vars.uniforms.vars.UniformVector2f +import com.mechanica.engine.shader.uniforms.vars.UniformVector2f import org.lwjgl.opengl.GL20.glUniform2f class LwjglVector2f( diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/LwjglVector3f.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/shader/uniforms/LwjglVector3f.kt similarity index 74% rename from desktop-application/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/LwjglVector3f.kt rename to desktop-application/src/main/kotlin/com/mechanica/engine/shader/uniforms/LwjglVector3f.kt index 842d30be..b876a3cd 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/LwjglVector3f.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/shader/uniforms/LwjglVector3f.kt @@ -1,7 +1,7 @@ -package com.mechanica.engine.shader.vars.uniforms +package com.mechanica.engine.shader.uniforms import com.mechanica.engine.shader.qualifiers.Qualifier -import com.mechanica.engine.shader.vars.uniforms.vars.UniformVector3f +import com.mechanica.engine.shader.uniforms.vars.UniformVector3f import org.lwjgl.opengl.GL20.glUniform3f class LwjglVector3f( diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/LwjglVector4f.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/shader/uniforms/LwjglVector4f.kt similarity index 76% rename from desktop-application/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/LwjglVector4f.kt rename to desktop-application/src/main/kotlin/com/mechanica/engine/shader/uniforms/LwjglVector4f.kt index 1e2b50a1..9c77da62 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vars/uniforms/LwjglVector4f.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/shader/uniforms/LwjglVector4f.kt @@ -1,7 +1,7 @@ -package com.mechanica.engine.shader.vars.uniforms +package com.mechanica.engine.shader.uniforms import com.mechanica.engine.shader.qualifiers.Qualifier -import com.mechanica.engine.shader.vars.uniforms.vars.UniformVector4f +import com.mechanica.engine.shader.uniforms.vars.UniformVector4f import org.lwjgl.opengl.GL20.glUniform4f class LwjglVector4f ( diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vbo/LwjglAttributeBuffer.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vbo/LwjglAttributeBuffer.kt index 6712a022..b7444e52 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vbo/LwjglAttributeBuffer.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vbo/LwjglAttributeBuffer.kt @@ -8,7 +8,7 @@ import com.mechanica.engine.vertices.AttributeArray import org.joml.Vector3f import org.joml.Vector4f import org.lwjgl.opengl.GL11.GL_FLOAT -import org.lwjgl.opengl.GL15.* +import org.lwjgl.opengl.GL15.glBufferSubData import org.lwjgl.opengl.GL20 import org.lwjgl.opengl.GL40 import java.nio.ByteBuffer @@ -36,7 +36,7 @@ class LwjglAttributeBuffer( GL40.glEnableVertexAttribArray(location) } - override fun disable() { + override fun unbind() { GL20.glDisableVertexAttribArray(location) } diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vbo/LwjglIndexArray.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vbo/LwjglIndexArray.kt index a32e21d6..55f6454e 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vbo/LwjglIndexArray.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/shader/vbo/LwjglIndexArray.kt @@ -16,7 +16,14 @@ class LwjglIndexArray(array: ShortArray) : LwjglVertexBuffer(array.s GL40.glBufferData(bufferTarget, array, GL20.GL_STATIC_DRAW) } - override fun subData(offset: Long, array: ShortArray) = glBufferSubData(bufferTarget, offset, array) + override fun unbind() { + GL40.glBindBuffer(bufferTarget, 0) + } + override fun subData(offset: Long, array: ShortArray) = glBufferSubData(bufferTarget, offset, array) + override fun set(array: ShortArray, from: Int, length: Int) { + vertexCount = array.size + super.set(array, from, length) + } } \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/text/FontAtlas.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/text/FontAtlas.kt index 07a28caf..166ab253 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/text/FontAtlas.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/text/FontAtlas.kt @@ -1,83 +1,166 @@ package com.mechanica.engine.text +import com.mechanica.engine.memory.BitmapBuffer +import com.mechanica.engine.memory.buffer.write +import com.mechanica.engine.memory.useMemoryStack import com.mechanica.engine.models.Image +import com.mechanica.engine.unit.vector.vec +import com.mechanica.engine.util.extensions.constrain import com.mechanica.engine.utils.loadImage import org.lwjgl.BufferUtils import org.lwjgl.opengl.GL11 -import org.lwjgl.stb.STBTTBakedChar -import org.lwjgl.stb.STBTTFontinfo +import org.lwjgl.stb.STBTTAlignedQuad +import org.lwjgl.stb.STBTTPackContext +import org.lwjgl.stb.STBTTPackedchar import org.lwjgl.stb.STBTruetype -import org.lwjgl.system.MemoryStack import java.nio.ByteBuffer import java.nio.IntBuffer +import kotlin.math.* -class FontAtlas(data: LwjglFont.FontData, val width: Int = 1024, val height: Int = 1024) : Image { +class FontAtlas(private val data: LwjglStandardFont.FontData, private val config: LwjglFontAtlasConfiguration) : Image { private val image: Image + val width: Int = config.width + val height: Int = config.height + override val id: Int get() = image.id val scale: Double + private val distanceFieldStart: Double + private val distanceFieldEnd: Double + + private val distanceFieldPadding: Double + get() = max(distanceFieldStart, distanceFieldEnd) + val padding: Float + get() = config.padding + if (config.distanceFieldsWasSet) distanceFieldPadding.toFloat() else 0f + + private val charRange = config.charRange.first.toInt()..config.charRange.last.toInt() + private val unknownIndex = 128 - charRange.first + init { val bitmap = BufferUtils.createByteBuffer(width * height) - bakeFont(bitmap, data.ttf, data.cdata) - scale = getBakedQuadScale(data.info, data.cdata) + + val correction = distanceFieldCorrection() + distanceFieldStart = config.sdfConfiguration.start + correction + distanceFieldEnd = config.sdfConfiguration.end + correction + + packFont(bitmap, data.ttf, data.cdata) + + if (config.distanceFieldsWasSet) { + insertCharacters(bitmap) + } + + scale = getScale(data) image = loadImage(bitmap, width, height, 1, GL11.GL_ALPHA) } + private fun insertCharacters(bitmap: ByteBuffer) { + val bitmapBuffer = BitmapBuffer(bitmap, width, height) + for (char in charRange){ + val glyph = createSDFGlyph(char.toChar()) + + val charAsInt = if (char in charRange) char - charRange.first else unknownIndex + val paddingVector = vec(padding/width.toFloat(), padding/height.toFloat()) + + if (glyph != null) { + useMemoryStack { + val q = STBTTAlignedQuad.mallocStack(this) + STBTruetype.stbtt_GetPackedQuad(data.cdata, width, height, charAsInt, floats(0f), floats(0f), q, false) + bitmapBuffer.insert(glyph, q.s0() - paddingVector.x.toFloat(), q.t0() - paddingVector.y.toFloat()) + } + } + } + bitmap.position(0) + } + + private fun getScale(data: LwjglStandardFont.FontData): Double { + val c = config.charRange.first + val x = data.cdata[0].xadvance() + return getAtlasScale(data.info, c, if (x >= 1f) x else 1f) + } + override fun bind() { image.bind() } - private fun bakeFont(bitmap: ByteBuffer, ttf: ByteBuffer, cdata: STBTTBakedChar.Buffer): Float { - var check: Int - var attempts = 0 - var maxFontHeight = 100f - var maxRow = 0 - var checkedFontHeight = maxFontHeight - - while (true) { - check = STBTruetype.stbtt_BakeFontBitmap(ttf, checkedFontHeight, bitmap, width, height, 32, cdata) - if (check == 0) { maxFontHeight = checkedFontHeight; break } - - if (attempts < 5) { - if (check > maxRow) { - maxRow = check - maxFontHeight = checkedFontHeight - } + private fun packFont(bitmap: ByteBuffer, ttf: ByteBuffer, cdata: STBTTPackedchar.Buffer) { + val characterHeight = config.characterSize - var error = 0f - if (check < 0) { - error = -0.3f*(1f + (check.toFloat())/ LwjglFont.charRange.count().toFloat()) - } else if (check > 0) { - error = 0.8f*(1f - check.toFloat()/ height.toFloat()) - } + STBTTPackContext.malloc().use { pc -> + STBTruetype.stbtt_PackBegin(pc, bitmap, width, height, 0, padding.toInt()*2) - checkedFontHeight *= (1f + error) - attempts++ - continue - } else { - break - } + val success = STBTruetype.stbtt_PackFontRange(pc, ttf, 0, characterHeight, charRange.first, cdata) + + if (!success) throw IllegalStateException("Error packing font into font atlas. The provided character size, ${config.characterSize}," + + " is likely too big for the provided atlas size, ${width}x$height") + + STBTruetype.stbtt_PackEnd(pc) } - STBTruetype.stbtt_BakeFontBitmap(ttf, maxFontHeight, bitmap, width, height, LwjglFont.charRange.first, cdata) - return maxFontHeight + + config.characterSize = characterHeight + } + + private fun checkPadding() { + val area = calculateAllowedCharacterArea() + val side = sqrt(area.toDouble()) + val paddingArea = padding*4*(side - padding) + val characterArea = area - paddingArea + + if (characterArea <= 0) throw IllegalStateException("Error packing font into font atlas. The provided padding, $padding," + + " is too much for the provided atlas size, ${width}x$height") + + } + + private fun calculateAllowedCharacterArea(): Int { + val bitmapArea = width*height + return bitmapArea/charRange.count() } + private fun distanceFieldCorrection(): Double { + val start = config.sdfConfiguration.start + val end = config.sdfConfiguration.end - private fun getBakedQuadScale(fontInfo: STBTTFontinfo, bakedFontData: STBTTBakedChar.Buffer): Double { - MemoryStack.stackPush().use { stack -> - val xAdvanceBuffer: IntBuffer = stack.mallocInt(1) - val lsbBuffer: IntBuffer = stack.mallocInt(1) - STBTruetype.stbtt_GetCodepointHMetrics(fontInfo, LwjglFont.charRange.first, xAdvanceBuffer, lsbBuffer) + return if (start*end > 0) { + val sign = sign(start) + -sign*min(abs(start), abs(end)) + } else 0.0 + } + + private fun createSDFGlyph(char: Char): BitmapBuffer? { + var start = distanceFieldStart + var end = distanceFieldEnd + + if (start*end > 0) { + val sign = sign(start) + val min = sign*min(abs(start), abs(end)) + start -= min + end -= min + } + + val edgeValue = getOnEdgeValue(start, end) - val xFromBakedData = bakedFontData[0].xadvance() - val xFromFontInfo = xAdvanceBuffer[0] + val pixelDistScale = 255f/(end - start).toFloat() - return xFromFontInfo.toDouble()/xFromBakedData + useMemoryStack { + val width: IntBuffer = ints(0) + val height: IntBuffer = ints(0) + val scale = STBTruetype.stbtt_ScaleForPixelHeight(data.info, config.characterSize) + + val buffer = STBTruetype.stbtt_GetCodepointSDF(data.info, scale, char.toInt(), distanceFieldPadding.toInt(), edgeValue.toByte(), pixelDistScale, + width, height, ints(0), ints(0)) + + if (buffer != null) return BitmapBuffer(buffer, width[0], height[0]) } + + return null + } + + private fun getOnEdgeValue(start: Double, end: Double): Int { + val startRatio = start/(end - start) + return ((1.0 + startRatio)*255).constrain(0.0, 255.0).toInt() } } \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/text/FontMetrics.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/text/FontMetrics.kt new file mode 100644 index 00000000..95b6cdc8 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/text/FontMetrics.kt @@ -0,0 +1,31 @@ +package com.mechanica.engine.text + +import org.lwjgl.stb.STBTTFontinfo +import org.lwjgl.stb.STBTruetype +import org.lwjgl.system.MemoryStack + +class FontMetrics(info: STBTTFontinfo, scale: Float = STBTruetype.stbtt_ScaleForPixelHeight(info, 1f)) { + val ascent: Float + val descent: Float + val lineGap: Float + + init { + var ascent = 0 + var descent = 0 + var lineGap = 0 + + MemoryStack.stackPush().use { stack -> + val ascentBuffer = stack.mallocInt(1) + val descentBuffer = stack.mallocInt(1) + val lineGapBuffer = stack.mallocInt(1) + STBTruetype.stbtt_GetFontVMetrics(info, ascentBuffer, descentBuffer, lineGapBuffer) + ascent = ascentBuffer[0] + descent = descentBuffer[0] + lineGap = lineGapBuffer[0] + } + + this.ascent = ascent*scale + this.descent = descent*scale + this.lineGap = lineGap*scale + } +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/text/FontUtils.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/text/FontUtils.kt new file mode 100644 index 00000000..2790a396 --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/text/FontUtils.kt @@ -0,0 +1,49 @@ +package com.mechanica.engine.text + +import com.mechanica.engine.unit.vector.DynamicVector +import org.lwjgl.stb.STBTTAlignedQuad +import org.lwjgl.stb.STBTTFontinfo +import org.lwjgl.stb.STBTruetype +import org.lwjgl.system.MemoryStack +import java.nio.IntBuffer +import kotlin.math.max + +fun STBTTAlignedQuad.copyToArrays(cursor: CharacterCursor, scale: Double, atlas: FontAtlas, p: Array, tc: Array) { + val i = max(cursor.nonWhiteSpaceIndex - 1, 0) *4 + + val padding = atlas.padding*1.01 + val texPaddingX = padding/atlas.width.toDouble() + val texPaddingY = padding/atlas.height.toDouble() + + tc[i + 0].x = s0().toDouble() - texPaddingX + tc[i + 0].y = t1().toDouble() + texPaddingY + tc[i + 1].x = s1().toDouble() + texPaddingX + tc[i + 1].y = t1().toDouble() + texPaddingY + tc[i + 2].x = s0().toDouble() - texPaddingX + tc[i + 2].y = t0().toDouble() - texPaddingY + tc[i + 3].x = s1().toDouble() + texPaddingX + tc[i + 3].y = t0().toDouble() - texPaddingY + + val yAdvance = cursor.yAdvance + p[i + 0].x = (x0() - padding)*scale + p[i + 0].y = (-y1() - padding)*scale + yAdvance + p[i + 1].x = (x1() + padding)*scale + p[i + 1].y = (-y1() - padding)*scale + yAdvance + p[i + 2].x = (x0() - padding)*scale + p[i + 2].y = (-y0() + padding)*scale + yAdvance + p[i + 3].x = (x1() + padding)*scale + p[i + 3].y = (-y0() + padding)*scale + yAdvance +} + + +fun getAtlasScale(fontInfo: STBTTFontinfo, testChar: Char, testCharXAdvance: Float): Double { + MemoryStack.stackPush().use { stack -> + val xAdvanceBuffer: IntBuffer = stack.mallocInt(1) + val lsbBuffer: IntBuffer = stack.mallocInt(1) + STBTruetype.stbtt_GetCodepointHMetrics(fontInfo, testChar.toInt(), xAdvanceBuffer, lsbBuffer) + + val xFromFontInfo = xAdvanceBuffer[0] + + return xFromFontInfo.toDouble()/ testCharXAdvance + } +} diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/text/LwjglFont.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/text/LwjglFont.kt deleted file mode 100644 index 91f32a69..00000000 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/text/LwjglFont.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.mechanica.engine.text - -import com.mechanica.engine.resources.Resource -import com.mechanica.engine.unit.vector.DynamicVector -import org.lwjgl.stb.STBTTAlignedQuad -import org.lwjgl.stb.STBTTBakedChar -import org.lwjgl.stb.STBTTFontinfo -import org.lwjgl.stb.STBTruetype -import org.lwjgl.stb.STBTruetype.stbtt_GetCodepointKernAdvance -import org.lwjgl.system.MemoryStack -import java.nio.ByteBuffer -import kotlin.math.max - -class LwjglFont(resource: Resource): Font() { - - override val ascent: Float - get() = metrics.ascent - override val descent: Float - get() = metrics.descent - override val lineGap: Float - get() = metrics.lineGap - - private val data = FontData(resource) - override val atlas = FontAtlas(data) - - private val metrics = FontMetrics() - - override fun addCharacterDataToArrays(cursor: CharacterCursor, positions: Array, texCoords: Array) { - val c = cursor.currentChar - val atlasScale = atlas.scale - val dataScale = data.scale - - val kern = stbtt_GetCodepointKernAdvance(data.info, cursor.previousChar.toInt(), c.toInt()).toFloat()/atlasScale.toFloat() - val charAsInt = if (c.toInt() in charRange) c.toInt() - charRange.first else unknownIndex - - MemoryStack.stackPush().use { stack -> - val x = stack.floats(cursor.xAdvance/(dataScale*atlasScale).toFloat() + kern) - val y = stack.floats(cursor.yAdvance) - val q = STBTTAlignedQuad.mallocStack(stack) - - STBTruetype.stbtt_GetBakedQuad(data.cdata, atlas.width, atlas.height, charAsInt, x, y, q, true) - cursor.xAdvance = (x[0]*dataScale*atlasScale).toFloat() - - if (c != ' ') copyToFloats(q, cursor, positions, texCoords) - } - } - - private fun copyToFloats(q: STBTTAlignedQuad, cursor: CharacterCursor, p: Array, tc: Array) { - val i = max(cursor.nonWhiteSpaceIndex - 1, 0)*4 - val atlasScale = atlas.scale - val dataScale = data.scale - - tc[i + 0].x = q.s0().toDouble() - tc[i + 0].y = q.t1().toDouble() - tc[i + 1].x = q.s1().toDouble() - tc[i + 1].y = q.t1().toDouble() - tc[i + 2].x = q.s0().toDouble() - tc[i + 2].y = q.t0().toDouble() - tc[i + 3].x = q.s1().toDouble() - tc[i + 3].y = q.t0().toDouble() - - val yAdvance = cursor.yAdvance - p[i + 0].x = (q.x0())*dataScale*atlasScale - p[i + 0].y = (-q.y1())*dataScale*atlasScale + yAdvance - p[i + 1].x = (q.x1())*dataScale*atlasScale - p[i + 1].y = (-q.y1())*dataScale*atlasScale + yAdvance - p[i + 2].x = (q.x0())*dataScale*atlasScale - p[i + 2].y = (-q.y0())*dataScale*atlasScale + yAdvance - p[i + 3].x = (q.x1())*dataScale*atlasScale - p[i + 3].y = (-q.y0())*dataScale*atlasScale + yAdvance - } - - inner class FontData(resource: Resource) { - val ttf: ByteBuffer = resource.buffer - val info: STBTTFontinfo = STBTTFontinfo.create() - val cdata: STBTTBakedChar.Buffer - val scale: Float - - init { - check(STBTruetype.stbtt_InitFont(info, ttf)) { "Failed to initialize font information." } - cdata = STBTTBakedChar.create(charRange.count()) - scale = STBTruetype.stbtt_ScaleForPixelHeight(info, 1f) - } - } - - private inner class FontMetrics { - val ascent: Float - val descent: Float - val lineGap: Float - - init { - var ascent = 0 - var descent = 0 - var lineGap = 0 - - MemoryStack.stackPush().use { stack -> - val ascentBuffer = stack.mallocInt(1) - val descentBuffer = stack.mallocInt(1) - val lineGapBuffer = stack.mallocInt(1) - STBTruetype.stbtt_GetFontVMetrics(data.info, ascentBuffer, descentBuffer, lineGapBuffer) - ascent = ascentBuffer[0] - descent = descentBuffer[0] - lineGap = lineGapBuffer[0] - } - - this.ascent = ascent*data.scale - this.descent = descent*data.scale - this.lineGap = lineGap*data.scale - } - } - companion object { - - val charRange = 32..128 - private val unknownIndex = 128 - charRange.first - - } -} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/text/LwjglFontAtlasConfiguration.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/text/LwjglFontAtlasConfiguration.kt new file mode 100644 index 00000000..e4a1490a --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/text/LwjglFontAtlasConfiguration.kt @@ -0,0 +1,57 @@ +package com.mechanica.engine.text + + +class LwjglFontAtlasConfiguration(initializer: FontAtlasConfiguration.() -> Unit) : FontAtlasConfiguration { + val anyWasSet: Boolean + get() = dimensionsWereSet || paddingWasSet || characterSizeWasSet || distanceFieldsWasSet + + var dimensionsWereSet = false + private set + override var width = 1024 + set(value) { + field = value + dimensionsWereSet = true + } + override var height = 1024 + set(value) { + field = value + dimensionsWereSet = true + } + + var characterSizeWasSet = false + private set + override var characterSize = 100f + set(value) { + field = value + characterSizeWasSet = true + } + + var paddingWasSet = false + private set + override var padding = 2f + set(value) { + field = value + paddingWasSet = true + } + + override var charRange = 32.toChar()..128.toChar() + + var distanceFieldsWasSet = false + val sdfConfiguration = FontAtlasConfiguration.SDFConfiguration() + override fun configureSDF(block: FontAtlasConfiguration.SDFConfiguration.() -> Unit) { + distanceFieldsWasSet = true + block(sdfConfiguration) + } + + init { + initializer(this) + } + + fun reset() { + paddingWasSet = false + characterSizeWasSet = false + distanceFieldsWasSet = false + dimensionsWereSet = false + } + +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/text/LwjglStandardFont.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/text/LwjglStandardFont.kt new file mode 100644 index 00000000..7a09f80b --- /dev/null +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/text/LwjglStandardFont.kt @@ -0,0 +1,65 @@ +package com.mechanica.engine.text + +import com.mechanica.engine.resources.Resource +import com.mechanica.engine.unit.vector.DynamicVector +import org.lwjgl.stb.STBTTAlignedQuad +import org.lwjgl.stb.STBTTFontinfo +import org.lwjgl.stb.STBTTPackedchar +import org.lwjgl.stb.STBTruetype +import org.lwjgl.stb.STBTruetype.stbtt_GetCodepointKernAdvance +import org.lwjgl.system.MemoryStack +import java.nio.ByteBuffer + +class LwjglStandardFont(resource: Resource, initializer: FontAtlasConfiguration.() -> Unit): Font() { + + override val ascent: Float + get() = metrics.ascent + override val descent: Float + get() = metrics.descent + override val lineGap: Float + get() = metrics.lineGap + + private val config = LwjglFontAtlasConfiguration(initializer) + private val charRange = config.charRange.first.toInt()..config.charRange.last.toInt() + private val unknownIndex = 128 - charRange.first + + private val data = FontData(resource) + + override val atlas = FontAtlas(data, config) + + private val metrics = FontMetrics(data.info, data.scale) + + override fun addCharacterDataToArrays(cursor: CharacterCursor, positions: Array, texCoords: Array) { + val c = cursor.currentChar + val atlasScale = atlas.scale + val dataScale = data.scale + + val kern = stbtt_GetCodepointKernAdvance(data.info, cursor.previousChar.toInt(), c.toInt()).toFloat()/atlasScale.toFloat() + val charAsInt = if (c.toInt() in charRange) c.toInt() - charRange.first else unknownIndex + + MemoryStack.stackPush().use { stack -> + val x = stack.floats(cursor.xAdvance/(dataScale*atlasScale).toFloat() + kern) + val y = stack.floats(cursor.yAdvance) + val q = STBTTAlignedQuad.mallocStack(stack) + + STBTruetype.stbtt_GetPackedQuad(data.cdata, atlas.width, atlas.height, charAsInt, x, y, q, true) + cursor.xAdvance = (x[0]*dataScale*atlasScale).toFloat() + + if (c != ' ') q.copyToArrays(cursor, dataScale*atlasScale, atlas, positions, texCoords) + } + } + + inner class FontData(resource: Resource) { + val ttf: ByteBuffer = resource.buffer + val info: STBTTFontinfo = STBTTFontinfo.create() + val cdata: STBTTPackedchar.Buffer + val scale: Float + + init { + check(STBTruetype.stbtt_InitFont(info, ttf)) { "Failed to initialize font information." } + cdata = STBTTPackedchar.create(charRange.count()) + scale = STBTruetype.stbtt_ScaleForPixelHeight(info, 1f) + } + } + +} \ No newline at end of file diff --git a/desktop-application/src/main/kotlin/com/mechanica/engine/utils/GLUtils.kt b/desktop-application/src/main/kotlin/com/mechanica/engine/utils/GLUtils.kt index b19dcc39..052edcce 100644 --- a/desktop-application/src/main/kotlin/com/mechanica/engine/utils/GLUtils.kt +++ b/desktop-application/src/main/kotlin/com/mechanica/engine/utils/GLUtils.kt @@ -59,7 +59,7 @@ class ImageData(resource: Resource) { val heightBuffer = BufferUtils.createIntBuffer(1) val componentsBuffer = BufferUtils.createIntBuffer(1) - data = STBImage.stbi_load_from_memory(resource.buffer, widthBuffer, heightBuffer, componentsBuffer, 4) + data = STBImage.stbi_load_from_memory(resource.buffer as ByteBuffer, widthBuffer, heightBuffer, componentsBuffer, 4) width = widthBuffer.get() height = heightBuffer.get() } diff --git a/mechanica-ui/build.gradle.kts b/mechanica-ui/build.gradle.kts new file mode 100644 index 00000000..b7dd86a1 --- /dev/null +++ b/mechanica-ui/build.gradle.kts @@ -0,0 +1,14 @@ + +plugins { + kotlin("jvm") + `java-library` +} + +dependencies { + implementation(project(":application-interface")) + implementation(project(":desktop-application")) + implementation(project(":mechanica")) + + api("com.dubulduke.ui:DukeUI:0.1") + +} \ No newline at end of file diff --git a/mechanica-ui/src/main/kotlin/com/mechanica/engine/ui/Events.kt b/mechanica-ui/src/main/kotlin/com/mechanica/engine/ui/Events.kt new file mode 100644 index 00000000..257b7e21 --- /dev/null +++ b/mechanica-ui/src/main/kotlin/com/mechanica/engine/ui/Events.kt @@ -0,0 +1,67 @@ +package com.mechanica.engine.ui + +import com.dubulduke.ui.event.Event +import com.dubulduke.ui.render.RenderDescription +import com.mechanica.engine.input.mouse.Mouse +import com.mechanica.engine.ui.style.Style +import com.mechanica.engine.util.EventBoolean + +class Events(description: RenderDescription