commit 8a13a99713d228400e60790fb0c165416f14d8a1 Author: DBotThePony Date: Wed Feb 2 22:48:12 2022 +0700 Оно существует diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..77f28785 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ + +build/ +unpacked_assets/ +.gradle/ +.idea/ diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..5d16cd8d --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,74 @@ + +plugins { + kotlin("jvm") version "1.6.10" + java + application +} + +group = "ru.dbotthepony" +version = "0.1-SNAPSHOT" + +val lwjglVersion = "3.3.0" +val lwjglNatives = "natives-windows" + +repositories { + mavenCentral() +} + +application { + mainClass.set("ru.dbotthepony.kstarbound.MainKt") +} + +java.toolchain.languageVersion.set(JavaLanguageVersion.of(17)) + +tasks.compileKotlin { + kotlinOptions { + jvmTarget = "17" + } +} + +dependencies { + implementation(kotlin("stdlib")) + + implementation("org.apache.logging.log4j:log4j-api:2.17.1") + implementation("org.apache.logging.log4j:log4j-core:2.17.1") + + testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + + implementation("com.google.code.gson:gson:2.8.9") + + implementation("it.unimi.dsi:fastutil:8.5.6") + + implementation("com.google.guava:guava:31.0.1-jre") + + implementation(platform("org.lwjgl:lwjgl-bom:$lwjglVersion")) + + implementation("org.lwjgl", "lwjgl") + implementation("org.lwjgl", "lwjgl-assimp") + implementation("org.lwjgl", "lwjgl-bgfx") + implementation("org.lwjgl", "lwjgl-glfw") + implementation("org.lwjgl", "lwjgl-nanovg") + implementation("org.lwjgl", "lwjgl-nuklear") + implementation("org.lwjgl", "lwjgl-openal") + implementation("org.lwjgl", "lwjgl-opengl") + implementation("org.lwjgl", "lwjgl-opus") + implementation("org.lwjgl", "lwjgl-par") + implementation("org.lwjgl", "lwjgl-stb") + implementation("org.lwjgl", "lwjgl-vulkan") + runtimeOnly("org.lwjgl", "lwjgl", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-assimp", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-bgfx", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-glfw", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-nanovg", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-nuklear", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-openal", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-opengl", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-opus", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-par", classifier = lwjglNatives) + runtimeOnly("org.lwjgl", "lwjgl-stb", classifier = lwjglNatives) +} + +tasks.getByName("test") { + useJUnitPlatform() +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..29e08e8c --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..7454180f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..ffed3a25 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 00000000..744e882e --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..e61f6e86 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "KStarBound" + diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/GameRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/GameRegistry.kt new file mode 100644 index 00000000..36ec703a --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/GameRegistry.kt @@ -0,0 +1,13 @@ +package ru.dbotthepony.kstarbound + +class GameRegistry { + private val table = HashMap() + val access: Map by table + + var frozen = false + private set + + fun freeze() { + frozen = true + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt new file mode 100644 index 00000000..7b9e86bf --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -0,0 +1,207 @@ +package ru.dbotthepony.kstarbound + +import org.apache.logging.log4j.LogManager +import org.lwjgl.Version +import org.lwjgl.glfw.Callbacks.glfwFreeCallbacks +import org.lwjgl.glfw.GLFW.* +import org.lwjgl.glfw.GLFWErrorCallback +import org.lwjgl.opengl.GL46.* +import org.lwjgl.system.MemoryStack.stackPush +import org.lwjgl.system.MemoryUtil.NULL +import ru.dbotthepony.kstarbound.gl.* +import ru.dbotthepony.kstarbound.math.* +import ru.dbotthepony.kstarbound.render.Camera +import ru.dbotthepony.kstarbound.render.ChunkRenderer +import ru.dbotthepony.kstarbound.render.TileRenderer +import ru.dbotthepony.kstarbound.world.CHUNK_SIZE +import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF +import ru.dbotthepony.kstarbound.world.ChunkTile +import java.io.File +import kotlin.math.PI + +private val LOGGER = LogManager.getLogger() +private const val TEST = false + +var viewportWidth = 800 + private set +var viewportHeight = 600 + private set + +var viewportMatrixGUI = updateViewportMatrixA() + private set + +var viewportMatrixGame = updateViewportMatrixB() + private set + +private fun updateViewportMatrixA(): Matrix4f { + return Matrix4f.ortho(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat(), 0.1f, 100f) +} + +private fun updateViewportMatrixB(): Matrix4f { + return Matrix4f.orthoDirect(0f, viewportWidth.toFloat(), 0f, viewportHeight.toFloat(), 0.1f, 100f) +} + +var window = 0L + private set + +fun main() { + LOGGER.info("Running LWJGL ${Version.getVersion()}") + + if (!TEST) { + try { + init() + loop() + } finally { + if (window != NULL) { + glfwFreeCallbacks(window) + glfwDestroyWindow(window) + } + + glfwTerminate() + glfwSetErrorCallback(null)?.free() + } + } else { + Starbound.addFilePath(File("./unpacked_assets/")) + Starbound.loadTileDefinition("alienrock") + } +} + +private fun init() { + GLFWErrorCallback.create { error, description -> + LOGGER.error("LWJGL error {}: {}", error, description) + }.set() + + check(glfwInit()) { "Unable to initialize GLFW" } + + glfwDefaultWindowHints() + + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE) + glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE) + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4) + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6) + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE) + + window = glfwCreateWindow(viewportWidth, viewportHeight, "LWJGL Window!", NULL, NULL) + require(window != NULL) { "Unable to create GLFW window" } + + glfwSetKeyCallback(window) { window, key, scancode, action, mods -> + if (key == GLFW_KEY_ESCAPE || key == GLFW_RELEASE) { + glfwSetWindowShouldClose(window, true) + } + } + + glfwSetFramebufferSizeCallback(window) { _, w, h -> + viewportWidth = w + viewportHeight = h + viewportMatrixGUI = updateViewportMatrixA() + viewportMatrixGame = updateViewportMatrixB() + glViewport(0, 0, w, h) + } + + val stack = stackPush() + + try { + val pWidth = stack.mallocInt(1) + val pHeight = stack.mallocInt(1) + + glfwGetWindowSize(window, pWidth, pHeight) + + val vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor())!! + + glfwSetWindowPos( + window, + (vidmode.width() - pWidth[0]) / 2, + (vidmode.height() - pHeight[0]) / 2 + ) + } finally { + stack.close() + } + + glfwMakeContextCurrent(window) + + // vsync + glfwSwapInterval(1) + + glfwShowWindow(window) + + Starbound.addFilePath(File("./unpacked_assets/")) + Starbound.loadTileDefinition("alienrock") +} + +private fun loop() { + val state = GLStateTracker() + val camera = Camera() + + // Set the clear color + glClearColor(0.75f, 0.75f, 0.75f, 0.75f) + + state.blend = true + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + val rock = ChunkTile(Starbound.loadTileDefinition("alienrock")) + val chunk = Starbound.world.setTile(Vector2i(2, 2), rock) + + chunk[3, 2] = rock + chunk[4, 2] = rock + chunk[4, 3] = rock + chunk[4, 4] = rock + chunk[3, 4] = rock + chunk[5, 4] = rock + + + for (x in 0 until 1) { + for (y in 0 until 4) { + //chunk[x, y] = ChunkTile(Starbound.loadTileDefinition("alienrock")) + } + } + + val chunkRenderer = ChunkRenderer(state, chunk) + chunkRenderer.tesselateStatic() + chunkRenderer.uploadStatic() + + // Run the rendering loop until the user has attempted to close + // the window or has pressed the ESCAPE key. + while (!glfwWindowShouldClose(window)) { + glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT) // clear the framebuffer + + state.matrixStack.clear(viewportMatrixGame.toMutableMatrix()) + + // program.use() + + // val time = glfwGetTime() + // program["globalColor"] = Uniform3f(sin(time).toFloat(), cos(time).toFloat(), sin(time).toFloat()) + // program["ourTexture"] = 0 + + // texture.bind() + // vao.bind() + // ebo.bind() + // glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0) + // checkForGLError() + + //tileRenderer.renderPiece() + + //state.shaderVertexTexture.use() + //state.shaderVertexTexture["_texture"] = 0 + ////state.shaderVertexTexture["_transform"] = Vector3f.FORWARD.rotateAroundThis(PI * glfwGetTime()) + ////state.shaderVertexTexture["_transform"] = state.matrixStack.push().scale((glfwGetTime() % 1000.0).toFloat(), (glfwGetTime() % 1000.0).toFloat(), (glfwGetTime() % 1000.0).toFloat()).last + ////state.shaderVertexTexture["_transform"] = Matrix4f.perspective(((glfwGetTime() * 16.0) % 180.0).toFloat(), 0.1f, 100f) + //state.shaderVertexTexture["_transform"] = state.matrixStack.push().scale(x = 10f, y = 10f).translateWithScale(10f, 10f).last + //texture.bind() + //texture.textureMinFilter = GL_NEAREST + //texture.textureMagFilter = GL_NEAREST + //chunkRenderer.bind() + //glDrawElements(GL_TRIANGLES, chunkRenderer.indexCount, GL_UNSIGNED_INT, 0) + //checkForGLError() + + state.matrixStack.push().scale(x = 100f, y = 100f).translateWithScale(0f, 0f) + chunkRenderer.render() + //state.matrixStack.translateWithScale(18f) + //chunkRenderer.render() + + glfwSwapBuffers(window) // swap the color buffers + + // Poll for window events. The key callback above will only be + // invoked during this call. + glfwPollEvents() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt new file mode 100644 index 00000000..57334633 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -0,0 +1,55 @@ +package ru.dbotthepony.kstarbound + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import ru.dbotthepony.kstarbound.defs.TileDefinition +import ru.dbotthepony.kstarbound.defs.TileDefinitionBuilder +import ru.dbotthepony.kstarbound.defs.TileRenderTemplate +import ru.dbotthepony.kstarbound.world.World +import java.io.File +import java.io.FileNotFoundException + +object Starbound { + val tiles = HashMap() + val world = World() + + private val _filepath = ArrayList() + val filepath = object : List by _filepath {} + + fun addFilePath(path: File) { + _filepath.add(path) + } + + fun findFile(path: File): File { + if (path.exists()) { + return path.canonicalFile + } + + for (sPath in _filepath) { + val newPath = File(sPath.path, path.path) + + if (newPath.exists()) { + return newPath + } + } + + throw FileNotFoundException("Unable to find $path in any of known file paths") + } + + fun findFile(path: String) = findFile(File(path)) + + fun loadJson(path: String): JsonElement { + if (path[0] == '/') + return JsonParser.parseReader(findFile(path.substring(1)).bufferedReader()) + + return JsonParser.parseReader(findFile(path).bufferedReader()) + } + + fun loadTileDefinition(name: String): TileDefinition { + return tiles.computeIfAbsent(name) { + val foundPath = findFile("tiles/materials/$name.material") + return@computeIfAbsent TileDefinitionBuilder.fromJson(JsonParser.parseReader(foundPath.bufferedReader()) as JsonObject).build(foundPath.parent) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/api/Structs.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/api/Structs.kt new file mode 100644 index 00000000..13a8ff08 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/api/Structs.kt @@ -0,0 +1,14 @@ +package ru.dbotthepony.kstarbound.api + +interface IStruct2f { + operator fun component1(): Float + operator fun component2(): Float +} + +interface IStruct3f : IStruct2f { + operator fun component3(): Float +} + +interface IStruct4F : IStruct3f { + operator fun component4(): Float +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/TileDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/TileDefinition.kt new file mode 100644 index 00000000..383bee61 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/TileDefinition.kt @@ -0,0 +1,477 @@ +package ru.dbotthepony.kstarbound.defs + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.math.Vector2i +import ru.dbotthepony.kstarbound.util.Color +import ru.dbotthepony.kstarbound.world.IChunk +import java.io.File + +data class TileDefinition( + val materialId: Int, + val materialName: String, + val particleColor: Color, + val itemDrop: String?, + val description: String, + val shortdescription: String, + + val racialDescription: ImmutableMap, + val footstepSound: String?, + val health: Int, + val category: String, + + val render: TileRenderDefinition +) { + init { + require(materialId >= 0) { "Material ID must be positive ($materialId given) ($materialName)" } + require(materialId != 0) { "Material ID 0 is reserved ($materialName)" } + } +} + +class TileDefinitionBuilder { + var materialId = 0 + var materialName = "unknown_tile" + var particleColor = Color.WHITE + var itemDrop: String? = "unknown" + var description = "..." + var shortdescription = "..." + + val racialDescription = ArrayList>() + + var footstepSound: String? = null + var health = 0 + var category = "generic" + + val render = TileRenderDefinitionBuilder() + + fun build(directory: String? = null): TileDefinition { + return TileDefinition( + racialDescription = ImmutableMap.builder().also { + for ((k, v) in this.racialDescription) { + it.put(k, v) + } + }.build(), + + materialId = materialId, + materialName = materialName, + particleColor = particleColor, + itemDrop = itemDrop, + description = description, + shortdescription = shortdescription, + + footstepSound = footstepSound, + health = health, + category = category, + + render = render.build(directory) + ) + } + + companion object { + fun fromJson(input: JsonObject): TileDefinitionBuilder { + val builder = TileDefinitionBuilder() + + try { + builder.materialName = input["materialName"].asString + builder.materialId = input["materialId"].asInt + + require(builder.materialId >= 0) { "Invalid materialId ${builder.materialId}" } + + builder.particleColor = Color(input["particleColor"].asJsonArray) + builder.itemDrop = input["itemDrop"].asString + builder.description = input["description"].asString + builder.shortdescription = input["shortdescription"].asString + builder.footstepSound = input["footstepSound"]?.asString + builder.health = input["health"].asInt + builder.category = input["category"].asString + + for (key in input.keySet()) { + if (key.endsWith("Description") && key.length != "Description".length) { + builder.racialDescription.add(key.substring(0, key.length - "Description".length) to input[key].asString) + } + } + + input["renderParameters"]?.asJsonObject?.let { + builder.render.texture = it["texture"].asString + builder.render.variants = it["variants"].asInt + builder.render.lightTransparent = it["lightTransparent"].asBoolean + builder.render.occludesBelow = it["occludesBelow"].asBoolean + builder.render.multiColored = it["multiColored"].asBoolean + builder.render.zLevel = it["zLevel"].asInt + } + + builder.render.renderTemplate = input["renderTemplate"]?.asString?.let renderTemplate@{ + return@renderTemplate TileRenderTemplate.load(it) + } + } catch(err: Throwable) { + throw IllegalArgumentException("Failed reading tile definition ${builder.materialName}", err) + } + + return builder + } + } +} + +/** + * Кусочек рендера тайла + * + * root.pieces[] + */ +data class TileRenderPiece( + val name: String, + val texture: File?, + val textureSize: Vector2i, + val texturePosition: Vector2i, + + val colorStride: Vector2i?, + val variantStride: Vector2i?, +) { + companion object { + fun fromJson(name: String, input: JsonObject): TileRenderPiece { + val texture = input["texture"]?.asString?.let { + if (it[0] != '/') { + throw UnsupportedOperationException("Render piece has not absolute texture path: $it") + } + + return@let File(it.substring(1)) + } + + val textureSize = Vector2i.fromJson(input["textureSize"].asJsonArray) + val texturePosition = Vector2i.fromJson(input["texturePosition"].asJsonArray) + + val colorStride = input["colorStride"]?.asJsonArray?.let { Vector2i.fromJson(it) } + val variantStride = input["variantStride"]?.asJsonArray?.let { Vector2i.fromJson(it) } + + return TileRenderPiece(name, texture, textureSize, texturePosition, colorStride, variantStride) + } + } +} + +/** + * Кусочек правила рендера тайла + * + * root.rules.`name`.entries[] + */ +sealed class RenderRule(params: Map) { + val matchHue = params["matchHue"] as? Boolean ?: false + val inverse = params["inverse"] as? Boolean ?: false + + abstract fun test(getter: IChunk, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean + + companion object { + fun factory(name: String, params: Map): RenderRule { + return when (name) { + "EqualsSelf" -> RenderRuleEqualsSelf(params) + "Shadows" -> RenderRuleShadows(params) + + else -> throw IllegalArgumentException("Unknown tile render rule $name") + } + } + + fun fromJson(input: JsonObject): RenderRule { + val params = ImmutableMap.builder() + + for (key in input.keySet()) { + if (key != "type") { + val value = input[key] as? JsonPrimitive + + if (value != null) { + if (value.isBoolean) { + params.put(key, value.asBoolean) + } else if (value.isNumber) { + params.put(key, value.asDouble) + } else { + params.put(key, value.asString) + } + } + } + } + + return factory(input["type"].asString, params.build()) + } + } +} + +class RenderRuleEqualsSelf(params: Map) : RenderRule(params) { + override fun test(getter: IChunk, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean { + val otherTile = getter[thisPos + offsetPos] ?: return inverse + + if (inverse) + return otherTile.def != thisRef + + return otherTile.def == thisRef + } +} + +class RenderRuleShadows(params: Map) : RenderRule(params) { + override fun test(getter: IChunk, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean { + return false // TODO + } +} + +enum class RenderRuleCombination { + ALL, + ANY +} + + +/** + * Правило рендера тайла + * + * root.rules[] + */ +data class TileRenderRule( + val name: String, + val join: RenderRuleCombination, + val pieces: List +) { + fun test(getter: IChunk, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean { + if (join == RenderRuleCombination.ANY) { + for (piece in pieces) { + if (piece.test(getter, thisRef, thisPos, offsetPos)) { + return true + } + } + + return false + } else { + for (piece in pieces) { + if (!piece.test(getter, thisRef, thisPos, offsetPos)) { + return false + } + } + + return true + } + } + + companion object { + fun fromJson(name: String, input: JsonObject): TileRenderRule { + val join = input["join"]?.asString?.let { + when (it) { + "any" -> RenderRuleCombination.ANY + else -> RenderRuleCombination.ALL + } + } ?: RenderRuleCombination.ALL + + val jEntries = input["entries"] as JsonArray + val pieces = ArrayList(jEntries.size()) + + for (elem in jEntries) { + pieces.add(RenderRule.fromJson(elem.asJsonObject)) + } + + return TileRenderRule(name, join, ImmutableList.copyOf(pieces)) + } + } +} + +data class TileRenderMatchedPiece( + val piece: TileRenderPiece, + val offset: Vector2i +) { + companion object { + fun fromJson(input: JsonArray, tilePieces: Map): TileRenderMatchedPiece { + val piece = input[0].asString.let { + return@let tilePieces[it] ?: throw IllegalArgumentException("Unable to find render piece $it") + } + + val offset = Vector2i.fromJson(input[1].asJsonArray) + return TileRenderMatchedPiece(piece, offset) + } + } +} + +data class TileRenderMatchPositioned( + val condition: TileRenderRule, + val offset: Vector2i +) { + /** + * Состояние [condition] на [thisPos] с [offset] + */ + fun test(getter: IChunk, thisRef: TileDefinition, thisPos: Vector2i): Boolean { + return condition.test(getter, thisRef, thisPos, offset) + } + + companion object { + fun fromJson(input: JsonArray, rulePieces: Map): TileRenderMatchPositioned { + val offset = Vector2i.fromJson(input[0].asJsonArray) + val condition = rulePieces[input[1].asString] ?: throw IllegalArgumentException("Rule ${input[1].asString} is missing!") + + return TileRenderMatchPositioned(condition, offset) + } + } +} + +data class TileRenderMatchPiece( + val pieces: List, + val matchAllPoints: List, + val subMatches: List +) { + /** + * Возвращает, сработали ли ВСЕ [matchAllPoints] + * + * Если хотя бы один из них вернул false, весь тест возвращает false + * + * [subMatches] стоит итерировать только если это вернуло true + */ + fun test(getter: IChunk, thisRef: TileDefinition, thisPos: Vector2i): Boolean { + for (matcher in matchAllPoints) { + if (!matcher.test(getter, thisRef, thisPos)) { + return false + } + } + + return true + } + + companion object { + fun fromJson(input: JsonObject, tilePieces: Map, rulePieces: Map): TileRenderMatchPiece { + val pieces = input["pieces"]?.asJsonArray?.let { + val list = ArrayList() + + for (thisPiece in it) { + list.add(TileRenderMatchedPiece.fromJson(thisPiece.asJsonArray, tilePieces)) + } + + return@let ImmutableList.copyOf(list) + } ?: listOf() + + val matchAllPoints = input["matchAllPoints"]?.asJsonArray?.let { + val list = ArrayList() + + for (thisPiece in it) { + list.add(TileRenderMatchPositioned.fromJson(thisPiece.asJsonArray, rulePieces)) + } + + return@let ImmutableList.copyOf(list) + } ?: listOf() + + val subMatches = input["subMatches"]?.asJsonArray?.let { + val list = ArrayList() + + for (thisPiece in it) { + list.add(fromJson(thisPiece.asJsonObject, tilePieces, rulePieces)) + } + + return@let ImmutableList.copyOf(list) + } ?: listOf() + + return TileRenderMatchPiece(pieces, matchAllPoints, subMatches) + } + } +} + +data class TileRenderMatch( + val name: String, + val pieces: List, +) { + companion object { + fun fromJson(input: JsonArray, tilePieces: Map, rulePieces: Map): TileRenderMatch { + val name = input[0].asString + val pieces = ArrayList() + + for (elem in input[1].asJsonArray) { + pieces.add(TileRenderMatchPiece.fromJson(elem.asJsonObject, tilePieces, rulePieces)) + } + + return TileRenderMatch(name, ImmutableList.copyOf(pieces)) + } + } +} + +data class TileRenderTemplate( + val representativePiece: String, + val pieces: Map, + val rules: Map, + val matches: Map, +) { + companion object { + val map = HashMap() + + fun load(path: String): TileRenderTemplate { + return map.computeIfAbsent(path) { + val json = Starbound.loadJson(path).asJsonObject + return@computeIfAbsent fromJson(json) + } + } + + fun fromJson(input: JsonObject): TileRenderTemplate { + val representativePiece = input["representativePiece"].asString + + val pieces = HashMap() + val rules = HashMap() + val matches = HashMap() + + val jPieces = input["pieces"] as JsonObject + + for (key in jPieces.keySet()) { + pieces[key] = TileRenderPiece.fromJson(key, jPieces[key] as JsonObject) + } + + val jRules = input["rules"] as JsonObject + + for (key in jRules.keySet()) { + rules[key] = TileRenderRule.fromJson(key, jRules[key] as JsonObject) + } + + val jMatches = input["matches"] as JsonArray + + for (instance in jMatches) { + val deserialized = TileRenderMatch.fromJson(instance.asJsonArray, pieces, rules) + matches[deserialized.name] = deserialized + } + + return TileRenderTemplate(representativePiece, ImmutableMap.copyOf(pieces), ImmutableMap.copyOf(rules), ImmutableMap.copyOf(matches)) + } + } +} + +data class TileRenderDefinition( + val texture: File, + val variants: Int, + val lightTransparent: Boolean, + val occludesBelow: Boolean, + val multiColored: Boolean, + val zLevel: Int, + val renderTemplate: TileRenderTemplate? +) + +class TileRenderDefinitionBuilder { + var texture = "" + var variants = 1 + var lightTransparent = false + var occludesBelow = false + var multiColored = false + var zLevel = 0 + var renderTemplate: TileRenderTemplate? = null + + fun build(directory: String? = null): TileRenderDefinition { + val newtexture: File + + if (texture[0] == '/') { + // путь абсолютен + newtexture = File(texture) + } else { + if (directory != null) { + newtexture = File(directory, texture) + } else { + newtexture = File(texture) + } + } + + return TileRenderDefinition( + texture = newtexture, + variants = variants, + lightTransparent = lightTransparent, + occludesBelow = occludesBelow, + multiColored = multiColored, + zLevel = zLevel, + renderTemplate = renderTemplate, + ) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/gl/ErrorCheck.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/ErrorCheck.kt new file mode 100644 index 00000000..907f6729 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/ErrorCheck.kt @@ -0,0 +1,34 @@ +package ru.dbotthepony.kstarbound.gl + +import org.lwjgl.opengl.GL46.* + +// GL_INVALID_ENUM +// GL_INVALID_VALUE +// GL_INVALID_OPERATION +// GL_STACK_OVERFLOW +// GL_STACK_UNDERFLOW +// GL_OUT_OF_MEMORY + +sealed class OpenGLError(message: String, val statusCode: Int) : Throwable(message) + +class OpenGLUnknownError(statusCode: Int, message: String = "Unknown OpenGL error occured: $statusCode") : OpenGLError(message, statusCode) + +class OpenGLInvalidEnumException(message: String = "Invalid enum provided") : OpenGLError(message, GL_INVALID_ENUM) +class OpenGLInvalidValueException(message: String = "Invalid value provided") : OpenGLError(message, GL_INVALID_VALUE) +class OpenGLInvalidOperationException(message: String = "Invalid operation in this context or invalid arguments provided") : OpenGLError(message, GL_INVALID_OPERATION) +class OpenGLStackOverflowException(message: String = "Stack overflow in OpenGL") : OpenGLError(message, GL_STACK_OVERFLOW) +class OpenGLStackUnderflowException(message: String = "Stack underflow in OpenGL") : OpenGLError(message, GL_STACK_UNDERFLOW) +class OpenGLOutOfMemoryException(message: String = "Out of Memory in OpenGL") : OpenGLError(message, GL_OUT_OF_MEMORY) + +fun checkForGLError() { + when (val errorCode = glGetError()) { + GL_NO_ERROR -> {} + GL_INVALID_ENUM -> throw OpenGLInvalidEnumException() + GL_INVALID_VALUE -> throw OpenGLInvalidValueException() + GL_INVALID_OPERATION -> throw OpenGLInvalidOperationException() + GL_STACK_OVERFLOW -> throw OpenGLStackOverflowException() + GL_STACK_UNDERFLOW -> throw OpenGLStackUnderflowException() + GL_OUT_OF_MEMORY -> throw OpenGLOutOfMemoryException() + else -> throw OpenGLUnknownError(errorCode) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLAttributeList.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLAttributeList.kt new file mode 100644 index 00000000..a13a0e38 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLAttributeList.kt @@ -0,0 +1,138 @@ +package ru.dbotthepony.kstarbound.gl + +import com.google.common.collect.ImmutableList +import org.lwjgl.opengl.GL46.* + +enum class GLType(val identity: Int, val typeIndentity: Int, val byteSize: Int, val logicalSize: Int) { + INT(GL_INT, GL_INT, 4, 1), + UINT(GL_UNSIGNED_INT, GL_UNSIGNED_INT, 4, 1), + FLOAT(GL_FLOAT, GL_FLOAT, 4, 1), + DOUBLE(GL_DOUBLE, GL_DOUBLE, 8, 1), + + VEC2F(GL_FLOAT_VEC2, GL_FLOAT, 8, 2), + VEC3F(GL_FLOAT_VEC3, GL_FLOAT, 12, 3), + VEC4F(GL_FLOAT_VEC4, GL_FLOAT, 16, 4), + + VEC2I(GL_INT_VEC2, GL_INT, 8, 2), + VEC3I(GL_INT_VEC3, GL_INT, 12, 3), + VEC4I(GL_INT_VEC4, GL_INT, 16, 4), + + MAT2F(GL_FLOAT_MAT2, GL_FLOAT, 2 * 2 * 4, 2 * 2), + MAT3F(GL_FLOAT_MAT3, GL_FLOAT, 3 * 3 * 4, 3 * 3), + MAT4F(GL_FLOAT_MAT4, GL_FLOAT, 4 * 4 * 4, 4 * 4), +} + +interface IGLAttributeList { + fun apply(target: GLVertexArrayObject, enable: Boolean = false) +} + +data class AttributeListPosition(val name: String, val index: Int, val glType: GLType, val stride: Int, val offset: Long) + +/** + * Хранит список аттрибутов для применения к Vertex Array Object + * + * Аттрибуты плотно упакованы и идут один за другим + * + * Создаётся через [GLFlatAttributeListBuilder] + */ +class GLFlatAttributeList(builder: GLFlatAttributeListBuilder) : IGLAttributeList { + val attributes: List + val size get() = attributes.size + val stride: Int + + operator fun get(index: Int) = attributes[index] + + fun vertexBuilder(vertexType: VertexType) = VertexBuilder(this, vertexType) + + init { + val buildList = ArrayList() + + var offset = 0L + var stride = 0 + + for (i in builder.attributes) { + stride += i.second.byteSize + } + + this.stride = stride + + // val counter = mutableMapOf() + + for (i in builder.attributes.indices) { + val value = builder.attributes[i].second + buildList.add(AttributeListPosition(builder.attributes[i].first, i, value, stride, offset)) + offset += value.byteSize + + // counter[value.typeIndentity] = (counter[value.typeIndentity] ?: 0) + 1 + } + + attributes = ImmutableList.copyOf(buildList) + } + + override fun apply(target: GLVertexArrayObject, enable: Boolean) { + for (i in attributes.indices) { + val value = attributes[i] + target.attribute(i, value.glType.logicalSize, value.glType.typeIndentity, false, value.stride, value.offset) + + if (enable) { + target.enableAttribute(i) + } + } + } + + companion object { + val VEC3F = GLFlatAttributeListBuilder().also {it.push(GLType.VEC3F)}.build() + val VERTEX_TEXTURE = GLFlatAttributeListBuilder().also {it.push(GLType.VEC3F).push(GLType.VEC2F)}.build() + } +} + +class GLFlatAttributeListBuilder : IGLAttributeList { + val attributes = ArrayList>() + + private fun findName(name: String): Boolean { + for (value in attributes) { + if (value.first == name) { + return true + } + } + + return false + } + + fun push(type: GLType): GLFlatAttributeListBuilder { + return push("$type#${attributes.size}", type) + } + + fun push(name: String, type: GLType): GLFlatAttributeListBuilder { + check(!findName(name)) { "Already has named attribute $name!" } + attributes.add(name to type) + return this + } + + fun build() = GLFlatAttributeList(this) + + @Deprecated("Используй build()") + override fun apply(target: GLVertexArrayObject, enable: Boolean) { + var offset = 0L + var stride = 0 + + for (i in attributes) { + stride += i.second.byteSize + } + + for (i in attributes.indices) { + val value = attributes[i].second + target.attribute(i, value.logicalSize, value.typeIndentity, false, stride, offset) + offset += value.byteSize + + if (enable) { + target.enableAttribute(i) + } + } + } + + companion object { + val VEC3F = GLFlatAttributeList.VEC3F + val VERTEX_TEXTURE = GLFlatAttributeList.VERTEX_TEXTURE + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLShader.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLShader.kt new file mode 100644 index 00000000..60be4047 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLShader.kt @@ -0,0 +1,47 @@ +package ru.dbotthepony.kstarbound.gl + +import org.lwjgl.opengl.GL20 +import org.lwjgl.opengl.GL46.* +import java.io.File +import kotlin.RuntimeException + +class ShaderCompilationException(reason: String) : RuntimeException(reason) + +class GLShader( + body: String, + type: Int +) { + constructor(body: File, type: Int) : this(body.also { require(it.exists()) { "Shader file does not exists: $body" } }.readText(), type) + + val pointer = glCreateShader(type) + var unlinked = false + private set + + init { + glShaderSource(pointer, body) + glCompileShader(pointer) + + val result = intArrayOf(0) + glGetShaderiv(pointer, GL_COMPILE_STATUS, result) + + if (result[0] == 0) { + throw ShaderCompilationException(glGetShaderInfoLog(pointer)) + } + + checkForGLError() + } + + fun unlink(): Boolean { + if (unlinked) + return false + + glDeleteShader(pointer) + checkForGLError() + return true + } + + companion object { + fun vertex(file: File) = GLShader(file, GL_VERTEX_SHADER) + fun fragment(file: File) = GLShader(file, GL_FRAGMENT_SHADER) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLShaderProgram.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLShaderProgram.kt new file mode 100644 index 00000000..609c0496 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLShaderProgram.kt @@ -0,0 +1,115 @@ +package ru.dbotthepony.kstarbound.gl + +import org.lwjgl.opengl.GL41 +import org.lwjgl.opengl.GL46.* +import ru.dbotthepony.kstarbound.api.IStruct3f +import ru.dbotthepony.kstarbound.api.IStruct4F +import ru.dbotthepony.kstarbound.math.FloatMatrix + +class ShaderLinkException(reason: String) : RuntimeException(reason) + +data class Uniform4f(val x: Float, val y: Float, val z: Float, val w: Float) : IStruct4F +data class Uniform3f(val x: Float, val y: Float, val z: Float) : IStruct3f + +class GLUniformLocation(val program: GLShaderProgram, val name: String, val pointer: Int) { + fun set(value: IStruct4F): GLUniformLocation { + program.state.ensureSameThread() + val (v0, v1, v2, v3) = value + glProgramUniform4f(program.pointer, pointer, v0, v1, v2, v3) + return this + } + + fun set(value: IStruct3f): GLUniformLocation { + program.state.ensureSameThread() + val (v0, v1, v2) = value + glProgramUniform3f(program.pointer, pointer, v0, v1, v2) + return this + } + + fun set(value: Int): GLUniformLocation { + program.state.ensureSameThread() + glProgramUniform1i(program.pointer, pointer, value) + return this + } + + fun set(value: FloatMatrix<*>): GLUniformLocation { + program.state.ensureSameThread() + + if (value.rows == 3 && value.columns == 3) { + // Матрица 3x3 + glProgramUniformMatrix3fv(program.pointer, pointer, false, value.toFloatArray()) + checkForGLError() + } else if (value.rows == 4 && value.columns == 4) { + // Матрица 4x4 + glProgramUniformMatrix4fv(program.pointer, pointer, false, value.toFloatArray()) + checkForGLError() + } else { + throw IllegalArgumentException("Can not use matrix with these dimensions: ${value.rows}x${value.columns}") + } + + return this + } +} + +class GLShaderProgram(val state: GLStateTracker, vararg shaders: GLShader) { + val pointer = glCreateProgram() + var linked = false + private set + + private val attached = HashSet() + val access = object : Collection by attached {} + + operator fun get(name: String): GLUniformLocation? { + state.ensureSameThread() + check(linked) { "Shader program is not linked!" } + + val location = glGetUniformLocation(pointer, name) + + if (location == -1) + return null + + return GLUniformLocation(this, name, location) + } + + operator fun set(name: String, value: Uniform4f) = this[name]?.set(value) + operator fun set(name: String, value: Uniform3f) = this[name]?.set(value) + operator fun set(name: String, value: Int) = this[name]?.set(value) + operator fun set(name: String, value: FloatMatrix<*>) = this[name]?.set(value) + + fun attach(shader: GLShader) { + state.ensureSameThread() + check(!linked) { "Already linked!" } + + if (!attached.add(shader)) { + throw IllegalStateException("Already attached! $shader") + } + + glAttachShader(pointer, shader.pointer) + } + + fun link() { + check(!linked) { "Already linked!" } + glLinkProgram(pointer) + + val success = intArrayOf(0) + glGetProgramiv(pointer, GL_LINK_STATUS, success) + + if (success[0] == 0) { + throw ShaderLinkException(glGetShaderInfoLog(pointer)) + } + + glGetError() + + linked = true + } + + fun use() = state.use(this) + + fun unlinkChildren() { + attached.forEach(GLShader::unlink) + } + + init { + shaders.forEach(this::attach) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLStateTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLStateTracker.kt new file mode 100644 index 00000000..558be675 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLStateTracker.kt @@ -0,0 +1,216 @@ +package ru.dbotthepony.kstarbound.gl + +import org.lwjgl.opengl.GL +import org.lwjgl.opengl.GL46.* +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.math.Matrix4f +import ru.dbotthepony.kstarbound.math.Matrix4fStack +import ru.dbotthepony.kstarbound.render.TileRenderer +import ru.dbotthepony.kstarbound.render.TileRenderers +import java.io.File +import kotlin.reflect.KProperty + +private class GLStateSwitchTracker(private val enum: Int, private var value: Boolean = false) { + operator fun getValue(glStateTracker: GLStateTracker, property: KProperty<*>): Boolean { + return value + } + + operator fun setValue(glStateTracker: GLStateTracker, property: KProperty<*>, value: Boolean) { + glStateTracker.ensureSameThread() + + if (value == this.value) + return + + if (value) { + glEnable(enum) + } else { + glDisable(enum) + } + + checkForGLError() + this.value = value + } +} + +class GLStateTracker { + init { + // This line is critical for LWJGL's interoperation with GLFW's + // OpenGL context, or any context that is managed externally. + // LWJGL detects the context that is current in the current thread, + // creates the GLCapabilities instance and makes the OpenGL + // bindings available for use. + GL.createCapabilities() + } + + var blend by GLStateSwitchTracker(GL_BLEND) + + var VBO: GLVertexBufferObject? = null + set(value) { + ensureSameThread() + if (field === value) return + field = value + + if (value == null) { + glBindBuffer(GL_ARRAY_BUFFER, 0) + checkForGLError() + return + } + + if (!value.isArray) throw IllegalArgumentException("Provided buffer object is not of Array type") + glBindBuffer(GL_ARRAY_BUFFER, value.pointer) + checkForGLError() + } + + var EBO: GLVertexBufferObject? = null + set(value) { + ensureSameThread() + if (field === value) return + field = value + + if (value == null) { + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0) + checkForGLError() + return + } + + if (!value.isElementArray) throw IllegalArgumentException("Provided buffer object is not of Array type") + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, value.pointer) + checkForGLError() + } + + var VAO: GLVertexArrayObject? = null + set(value) { + ensureSameThread() + if (field === value) return + field = value + + if (value == null) { + glBindVertexArray(0) + checkForGLError() + return + } + + glBindVertexArray(value.pointer) + checkForGLError() + } + + var program: GLShaderProgram? = null + private set + + var texture2D: GLTexture2D? = null + set(value) { + ensureSameThread() + if (field === value) return + field = value + if (value == null) return + glBindTexture(GL_TEXTURE_2D, value.pointer) + checkForGLError() + } + + var activeTexture = 0 + set(value) { + ensureSameThread() + if (field == value) return + require(value >= 0) { "Invalid texture block $value" } + require(value < 80) { "Too big texture block index $value, OpenGL 4.6 guarantee only 80!" } + field = value + glActiveTexture(GL_TEXTURE0 + value) + checkForGLError() + } + + init { + glActiveTexture(GL_TEXTURE0) + checkForGLError() + } + + val thread = Thread.currentThread() + val tileRenderers = TileRenderers(this) + + fun ensureSameThread() { + if (thread !== Thread.currentThread()) { + throw IllegalAccessException("Trying to access $this outside of $thread!") + } + } + + fun isSameThread() = thread === Thread.currentThread() + + fun program(vararg shaders: GLShader): GLShaderProgram { + return GLShaderProgram(this, *shaders) + } + + fun newVBO(type: VBOType = VBOType.ARRAY): GLVertexBufferObject { + return GLVertexBufferObject(this, type) + } + + fun newEBO() = newVBO(VBOType.ELEMENT_ARRAY) + fun newVAO() = GLVertexArrayObject(this) + fun newTexture(name: String = "") = GLTexture2D(this, name) + + private val named2DTextures = HashMap() + + fun loadNamedTexture(path: File, memoryFormat: Int = GL_RGBA, fileFormat: Int = GL_RGBA): GLTexture2D { + val found = Starbound.findFile(path) + + return named2DTextures.computeIfAbsent(found.absolutePath) { + return@computeIfAbsent newTexture(found.absolutePath).upload(found, memoryFormat, fileFormat).generateMips() + } + } + + fun bind(obj: GLVertexBufferObject): GLVertexBufferObject { + if (obj.type == VBOType.ARRAY) + VBO = obj + else + EBO = obj + + return obj + } + + fun unbind(obj: GLVertexBufferObject): GLVertexBufferObject { + if (obj.type == VBOType.ARRAY) + if (obj == VBO) + VBO = null + else + if (obj == EBO) + EBO = null + + return obj + } + + fun bind(obj: GLVertexArrayObject): GLVertexArrayObject { + VAO = obj + return obj + } + + fun unbind(obj: GLVertexArrayObject): GLVertexArrayObject { + if (obj == VAO) + VAO = null + + return obj + } + + fun use(obj: GLShaderProgram): GLShaderProgram { + ensureSameThread() + + if (obj == program) { + return obj + } + + check(obj.linked) { "Program is not linked!" } + + program = obj + glUseProgram(obj.pointer) + checkForGLError() + return obj + } + + val shaderVertexTexture = program( + GLShader.fragment(File("./src/main/resources/shaders/f_texture.glsl")), + GLShader.vertex(File("./src/main/resources/shaders/v_vertex_texture.glsl")) + ).also { + it.link() + it.unlinkChildren() + it["_transform"] = Matrix4f.IDENTITY + } + + val matrixStack = Matrix4fStack() +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLTexture.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLTexture.kt new file mode 100644 index 00000000..106900a3 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLTexture.kt @@ -0,0 +1,146 @@ +package ru.dbotthepony.kstarbound.gl + +import org.apache.logging.log4j.LogManager +import org.lwjgl.opengl.GL46.* +import org.lwjgl.stb.STBImage +import ru.dbotthepony.kstarbound.math.Vector2i +import java.io.File +import java.io.FileNotFoundException +import java.nio.ByteBuffer +import kotlin.reflect.KProperty + +class TextureLoadingException(message: String) : Throwable(message) + +data class UVCoord(val u: Float, val v: Float) + +class GLTexturePropertyTracker(private val flag: Int, var value: Int) { + operator fun getValue(thisRef: GLTexture2D, property: KProperty<*>): Int { + return value + } + + operator fun setValue(thisRef: GLTexture2D, property: KProperty<*>, value: Int) { + thisRef.state.ensureSameThread() + if (this.value == value) return + this.value = value + glTextureParameteri(thisRef.pointer, flag, value) + checkForGLError() + } +} + +class GLTexture2D(val state: GLStateTracker, val name: String = "") { + val pointer = glGenTextures() + + var width = 0 + private set + + var height = 0 + private set + + var uploaded = false + private set + + private var mipsWarning = 2 + + var textureMinFilter by GLTexturePropertyTracker(GL_TEXTURE_MIN_FILTER, GL_LINEAR) + var textureMagFilter by GLTexturePropertyTracker(GL_TEXTURE_MAG_FILTER, GL_LINEAR) + + var textureWrapS by GLTexturePropertyTracker(GL_TEXTURE_WRAP_S, GL_REPEAT) + var textureWrapT by GLTexturePropertyTracker(GL_TEXTURE_WRAP_T, GL_REPEAT) + + fun bind(): GLTexture2D { + if (mipsWarning == 1) { + LOGGER.warn("(Likely) Trying to use texture {} before generated it's mips, this probably won't work!", this) + mipsWarning = 0 + } else if (mipsWarning == 2) { + mipsWarning = 1 + } + + state.texture2D = this + return this + } + + fun generateMips(): GLTexture2D { + state.ensureSameThread() + glGenerateTextureMipmap(pointer) + checkForGLError() + mipsWarning = 0 + return this + } + + fun pixelToUV(x: Float, y: Float): UVCoord { + check(uploaded) { "Texture is not uploaded to be used" } + return UVCoord(x / width, y / height) + } + + fun pixelToUV(x: Int, y: Int): UVCoord { + check(uploaded) { "Texture is not uploaded to be used" } + return UVCoord(x.toFloat() / width, y.toFloat() / height) + } + + fun pixelToUV(pos: Vector2i) = pixelToUV(pos.x, pos.y) + + private fun upload(mipmap: Int, loadedFormat: Int, width: Int, height: Int, bufferFormat: Int, dataFormat: Int, data: IntArray): GLTexture2D { + bind() + + require(width > 0) { "Invalid width $width" } + require(height > 0) { "Invalid height $height" } + this.width = width + this.height = height + glTexImage2D(GL_TEXTURE_2D, mipmap, loadedFormat, width, height, 0, bufferFormat, dataFormat, data) + checkForGLError() + uploaded = true + return this + } + + private fun upload(mipmap: Int, memoryFormat: Int, width: Int, height: Int, bufferFormat: Int, dataFormat: Int, data: ByteBuffer): GLTexture2D { + bind() + + require(width > 0) { "Invalid width $width" } + require(height > 0) { "Invalid height $height" } + + this.width = width + this.height = height + glTexImage2D(GL_TEXTURE_2D, mipmap, memoryFormat, width, height, 0, bufferFormat, dataFormat, data) + checkForGLError() + uploaded = true + return this + } + + fun upload(memoryFormat: Int, width: Int, height: Int, bufferFormat: Int, dataFormat: Int, data: IntArray): GLTexture2D { + return upload(0, memoryFormat, width, height, bufferFormat, dataFormat, data) + } + + fun upload(memoryFormat: Int, width: Int, height: Int, bufferFormat: Int, dataFormat: Int, data: ByteBuffer): GLTexture2D { + return upload(0, memoryFormat, width, height, bufferFormat, dataFormat, data) + } + + fun upload(path: File, memoryFormat: Int, bufferFormat: Int): GLTexture2D { + state.ensureSameThread() + + if (!path.exists()) { + throw FileNotFoundException("${path.absolutePath} does not exist") + } + + if (!path.isFile) { + throw FileNotFoundException("${path.absolutePath} is not a file") + } + + val getwidth = intArrayOf(0) + val getheight = intArrayOf(0) + val getchannels = intArrayOf(0) + + val bytes = STBImage.stbi_load(path.absolutePath, getwidth, getheight, getchannels, 0) ?: throw TextureLoadingException("Unable to load ${path.absolutePath}. Is it a valid image?") + + require(getwidth[0] > 0) { "Image ${path.absolutePath} has bad width of ${getwidth[0]}" } + require(getheight[0] > 0) { "Image ${path.absolutePath} has bad height of ${getheight[0]}" } + + upload(memoryFormat, getwidth[0], getheight[0], bufferFormat, GL_UNSIGNED_BYTE, bytes) + STBImage.stbi_image_free(bytes) + + return this + } + + companion object { + private val LOGGER = LogManager.getLogger() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLVertexArrayObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLVertexArrayObject.kt new file mode 100644 index 00000000..9afa2eff --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLVertexArrayObject.kt @@ -0,0 +1,50 @@ +package ru.dbotthepony.kstarbound.gl + +import org.lwjgl.opengl.GL46.* +import java.io.Closeable + +class GLVertexArrayObject(val state: GLStateTracker) : Closeable { + val pointer = glGenVertexArrays() + + fun bind(): GLVertexArrayObject { + check(isValid) { "Tried to use NULL GLVertexArrayObject" } + return state.bind(this) + } + + fun unbind(): GLVertexArrayObject { + check(isValid) { "Tried to use NULL GLVertexArrayObject" } + return state.unbind(this) + } + + fun attribute(position: Int, size: Int, type: Int, normalize: Boolean, stride: Int, offset: Long = 0L): GLVertexArrayObject { + check(isValid) { "Tried to use NULL GLVertexArrayObject" } + state.ensureSameThread() + glVertexAttribPointer(position, size, type, normalize, stride, offset) + checkForGLError() + return this + } + + fun enableAttribute(position: Int): GLVertexArrayObject { + check(isValid) { "Tried to use NULL GLVertexArrayObject" } + state.ensureSameThread() + glEnableVertexArrayAttrib(pointer, position) + //glEnableVertexAttribArray(position) + checkForGLError() + return this + } + + var isValid = true + private set + + override fun close() { + state.ensureSameThread() + if (isValid) return + + if (state.VAO == this) { + state.VAO = null + } + + glDeleteVertexArrays(pointer) + isValid = false + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLVertexBufferObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLVertexBufferObject.kt new file mode 100644 index 00000000..05020ee2 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/GLVertexBufferObject.kt @@ -0,0 +1,84 @@ +package ru.dbotthepony.kstarbound.gl + +import org.lwjgl.opengl.GL46.* +import java.io.Closeable +import java.nio.ByteBuffer + +enum class VBOType(val value: Int) { + ARRAY(GL_ARRAY_BUFFER), + ELEMENT_ARRAY(GL_ELEMENT_ARRAY_BUFFER), +} + +class GLVertexBufferObject(val state: GLStateTracker, val type: VBOType = VBOType.ARRAY) : Closeable { + val pointer = glGenBuffers() + + val isArray get() = type == VBOType.ARRAY + val isElementArray get() = type == VBOType.ELEMENT_ARRAY + + fun bind(): GLVertexBufferObject { + check(isValid) { "Tried to use NULL GLVertexBufferObject" } + state.bind(this) + return this + } + + fun unbind(): GLVertexBufferObject { + check(isValid) { "Tried to use NULL GLVertexBufferObject" } + state.unbind(this) + return this + } + + fun bufferData(data: ByteBuffer, usage: Int): GLVertexBufferObject { + check(isValid) { "Tried to use NULL GLVertexBufferObject" } + state.ensureSameThread() + glNamedBufferData(pointer, data, usage) + checkForGLError() + return this + } + + fun bufferData(data: IntArray, usage: Int): GLVertexBufferObject { + check(isValid) { "Tried to use NULL GLVertexBufferObject" } + state.ensureSameThread() + glNamedBufferData(pointer, data, usage) + checkForGLError() + return this + } + + fun bufferData(data: FloatArray, usage: Int): GLVertexBufferObject { + check(isValid) { "Tried to use NULL GLVertexBufferObject" } + state.ensureSameThread() + glNamedBufferData(pointer, data, usage) + checkForGLError() + return this + } + + fun bufferData(data: DoubleArray, usage: Int): GLVertexBufferObject { + check(isValid) { "Tried to use NULL GLVertexBufferObject" } + state.ensureSameThread() + glNamedBufferData(pointer, data, usage) + checkForGLError() + return this + } + + fun bufferData(data: LongArray, usage: Int): GLVertexBufferObject { + check(isValid) { "Tried to use NULL GLVertexBufferObject" } + state.ensureSameThread() + glNamedBufferData(pointer, data, usage) + checkForGLError() + return this + } + + var isValid = true + private set + + override fun close() { + state.ensureSameThread() + if (!isValid) return + + if (state.VBO == this) { + state.VBO = null + } + + glDeleteBuffers(pointer) + isValid = false + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/gl/VertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/VertexBuilder.kt new file mode 100644 index 00000000..aa93e336 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/gl/VertexBuilder.kt @@ -0,0 +1,202 @@ +package ru.dbotthepony.kstarbound.gl + +import org.lwjgl.opengl.GL46.* +import java.nio.ByteBuffer +import java.nio.ByteOrder + +enum class VertexType(val elements: Int, val indicies: IntArray) { + TRIANGLES(3, intArrayOf(0, 1, 2)), + QUADS(4, intArrayOf(0, 1, 2, 1, 2, 3)) +} + +typealias VertexTransformer = (VertexBuilder.Vertex, Int) -> VertexBuilder.Vertex +private val emptyTransform: VertexTransformer = { it, _ -> it } + +object VertexTransformers { + fun uv(u0: Float, + v0: Float, + u1: Float, + v1: Float, + lambda: VertexTransformer = emptyTransform): VertexTransformer { + return transformer@{ it, index -> + when (index) { + 0 -> it.pushVec2f(u0, v0) + 1 -> it.pushVec2f(u1, v0) + 2 -> it.pushVec2f(u0, v1) + 3 -> it.pushVec2f(u1, v1) + } + + return@transformer lambda(it, index) + } + } +} + +class VertexBuilder(val attributes: GLFlatAttributeList, private val type: VertexType) { + private val verticies = ArrayList() + val indexCount get() = (verticies.size / type.elements) * type.indicies.size + + fun begin(): VertexBuilder { + verticies.clear() + return this + } + + fun vertex(): Vertex { + return Vertex() + } + + fun quadZ( + x0: Float, + y0: Float, + x1: Float, + y1: Float, + z: Float, + lambda: VertexTransformer = emptyTransform + ): VertexBuilder { + check(type == VertexType.QUADS) { "Currently building $type" } + + lambda(Vertex().pushVec3f(x0, y0, z), 0).end() + lambda(Vertex().pushVec3f(x1, y0, z), 1).end() + lambda(Vertex().pushVec3f(x0, y1, z), 2).end() + lambda(Vertex().pushVec3f(x1, y1, z), 3).end() + + return this + } + + fun checkValid() { + for (vertex in verticies) { + vertex.checkValid() + } + } + + /** + * Загружает буфер в указанные VBO и EBO + * + * операция создаёт мусор вне кучи и довольно медленная + */ + fun upload(vbo: GLVertexBufferObject, ebo: GLVertexBufferObject, drawType: Int = GL_DYNAMIC_DRAW) { + require(vbo.isArray) { "$vbo is not an array" } + require(ebo.isElementArray) { "$vbo is not an element array" } + + checkValid() + + check(verticies.size % type.elements == 0) { "Not fully built (expected ${type.elements} verticies to be present for each element, last element has only ${verticies.size % type.elements})" } + + vbo.bind() + ebo.bind() + + if (verticies.size == 0) { + vbo.bufferData(intArrayOf(), drawType) + ebo.bufferData(intArrayOf(), drawType) + return + } + + val bytes = ByteBuffer.allocateDirect(verticies.size * attributes.stride) + bytes.order(ByteOrder.nativeOrder()) + + for (vertex in verticies) { + vertex.upload(bytes) + } + + check(bytes.position() == bytes.capacity()) { "Buffer is not fully filled (position: ${bytes.position()}; capacity: ${bytes.capacity()})" } + + bytes.position(0) + vbo.bufferData(bytes, drawType) + + val elementIndicies = IntArray((verticies.size / type.elements) * type.indicies.size) + var offset = 0 + var offsetVertex = 0 + + for (i in 0 until verticies.size / type.elements) { + for (i2 in type.indicies.indices) { + elementIndicies[offset + i2] = type.indicies[i2] + offsetVertex + } + + offset += type.indicies.size + offsetVertex += type.elements + } + + ebo.bufferData(elementIndicies, drawType) + } + + inner class Vertex { + init { + verticies.add(this) + } + + private val store = arrayOfNulls(attributes.size) + private var index = 0 + + fun upload(bytes: ByteBuffer) { + for (element in store) { + when (element) { + is FloatArray -> for (i in element) bytes.putFloat(i) + is IntArray -> for (i in element) bytes.putInt(i) + is ByteArray -> for (i in element) bytes.put(i) + is DoubleArray -> for (i in element) bytes.putDouble(i) + else -> throw IllegalStateException("Unknown element $element") + } + } + } + + override fun toString(): String { + return "Vertex(${store.map { + return@map when (it) { + is FloatArray -> it.joinToString(", ") + is IntArray -> it.joinToString(", ") + is ByteArray -> it.joinToString(", ") + is DoubleArray -> it.joinToString(", ") + else -> "null" + } }.joinToString("; ")})" + } + + fun expect(name: String): Vertex { + if (index >= attributes.size) { + throw IllegalStateException("Reached end of attribute list early, expected $name") + } + + if (attributes[index].name != name) { + throw IllegalStateException("Expected $name, got ${attributes[index].name}[${attributes[index].glType}] (at position $index)") + } + + return this + } + + fun expect(type: GLType): Vertex { + if (index >= attributes.size) { + throw IllegalStateException("Reached end of attribute list early, expected type $type") + } + + if (attributes[index].glType != type) { + throw IllegalStateException("Expected $type, got ${attributes[index].name}[${attributes[index].glType}] (at position $index)") + } + + return this + } + + fun pushVec3f(x: Float, y: Float, z: Float): Vertex { + expect(GLType.VEC3F) + store[index++] = floatArrayOf(x, y, z) + return this + } + + fun pushVec2f(x: Float, y: Float): Vertex { + expect(GLType.VEC2F) + store[index++] = floatArrayOf(x, y) + return this + } + + fun checkValid() { + for (elem in store.indices) { + if (store[elem] == null) { + throw IllegalStateException("Vertex element at position $elem is null") + } + } + } + + fun end(): VertexBuilder { + checkValid() + return this@VertexBuilder + } + } +} + diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Angle.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Angle.kt new file mode 100644 index 00000000..1c89f45b --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Angle.kt @@ -0,0 +1,50 @@ +package ru.dbotthepony.kstarbound.math + +import kotlin.math.cos +import kotlin.math.sin + +interface IAngle { + val pitch: Double + val yaw: Double + val roll: Double + + fun matrixX(): Matrix4f { + val s = sin(pitch).toFloat() + val c = cos(pitch).toFloat() + + return Matrix4f( + m11 = c, m12 = -s, + m21 = s, m22 = c, + ) + } + + fun matrixY(): Matrix4f { + val s = sin(yaw).toFloat() + val c = cos(yaw).toFloat() + + return Matrix4f( + m00 = c, m02 = s, + m20 = -s, m22 = c + ) + } + + fun matrixZ(): Matrix4f { + val s = sin(roll).toFloat() + val c = cos(roll).toFloat() + + return Matrix4f( + m00 = c, m01 = -s, + m10 = s, m11 = c + ) + } + + fun matrixXYZ(): Matrix4f { + return matrixX() * matrixY() * matrixZ() + } +} + +data class Angle( + override val pitch: Double = 0.0, + override val yaw: Double = 0.0, + override val roll: Double = 0.0, +) : IAngle diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Matrix.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Matrix.kt new file mode 100644 index 00000000..a0d4fe62 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Matrix.kt @@ -0,0 +1,851 @@ +package ru.dbotthepony.kstarbound.math + +import kotlin.math.PI +import kotlin.math.tan + +interface IMatrixLike { + val columns: Int + val rows: Int +} + +interface IMatrixLikeGetterI { + operator fun get(row: Int, column: Int): Int +} + +interface IMatrixLikeFloat : IMatrixLike { + operator fun get(row: Int, column: Int): Float +} + +interface IMatrix : IMatrixLike { + operator fun plus(other: IMatrix): IMatrix + operator fun minus(other: IMatrix): IMatrix + operator fun times(other: IMatrix): IMatrix +} + +interface FloatMatrix> : IMatrix, IMatrixLikeFloat { + operator fun plus(other: Float): T + operator fun minus(other: Float): T + operator fun times(other: Float): T + operator fun div(other: Float): T + + override operator fun plus(other: IMatrix): T + override operator fun minus(other: IMatrix): T + override operator fun times(other: IMatrix): T + + /** + * Если матрица больше или меньше, считать что всевозможные остальные координаты равны единице (не менять) + */ + fun scale(x: Float, y: Float = 1f, z: Float = 1f, w: Float = 1f): T + + fun scale(value: Vector4f) = scale(value.x, value.y, value.z, value.w) + + fun translate(x: Float = 0f, y: Float = 0f, z: Float = 0f): T + fun translate(vector3f: Vector3f) = translate(vector3f.x, vector3f.y, vector3f.z) + + fun translateWithScale(x: Float = 0f, y: Float = 0f, z: Float = 0f): T + fun translateWithScale(vector3f: Vector3f) = translateWithScale(vector3f.x, vector3f.y, vector3f.z) + + /** + * Выдает массив в готовом для OpenGL виде (строка -> столбец) по умолчанию + */ + fun toFloatArray(columnMajor: Boolean = true): FloatArray { + val buff = FloatArray(rows * columns) + + if (columnMajor) { + for (row in 0 until rows) { + for (column in 0 until columns) { + buff[row + rows * column] = this[row, column] + } + } + } else { + for (row in 0 until rows) { + for (column in 0 until columns) { + buff[row * columns + column] = this[row, column] + } + } + } + + return buff + } +} + +interface MutableFloatMatrix> : FloatMatrix { + operator fun set(row: Int, column: Int, value: Float) +} + +abstract class AbstractMatrix4f> : FloatMatrix { + abstract val m00: Float; abstract val m01: Float; abstract val m02: Float; abstract val m03: Float + abstract val m10: Float; abstract val m11: Float; abstract val m12: Float; abstract val m13: Float + abstract val m20: Float; abstract val m21: Float; abstract val m22: Float; abstract val m23: Float + abstract val m30: Float; abstract val m31: Float; abstract val m32: Float; abstract val m33: Float + + override val columns: Int + get() = 4 + + override val rows: Int + get() = 4 + + override fun get(row: Int, column: Int): Float { + return when (column) { + 0 -> when (row) { + 0 -> m00 + 1 -> m10 + 2 -> m20 + 3 -> m30 + else -> throw IndexOutOfBoundsException("Row: $row") + } + + 1 -> when (row) { + 0 -> m01 + 1 -> m11 + 2 -> m21 + 3 -> m31 + else -> throw IndexOutOfBoundsException("Row: $row") + } + + 2 -> when (row) { + 0 -> m02 + 1 -> m12 + 2 -> m22 + 3 -> m32 + else -> throw IndexOutOfBoundsException("Row: $row") + } + + 3 -> when (row) { + 0 -> m03 + 1 -> m13 + 2 -> m23 + 3 -> m33 + else -> throw IndexOutOfBoundsException("Row: $row") + } + + else -> throw IndexOutOfBoundsException("Column: $column") + } + } + + protected abstract fun createOrModify( + m00: Float, m01: Float, m02: Float, m03: Float, + m10: Float, m11: Float, m12: Float, m13: Float, + m20: Float, m21: Float, m22: Float, m23: Float, + m30: Float, m31: Float, m32: Float, m33: Float, + ): T + + override fun plus(other: IMatrix): T { + if (other !is FloatMatrix<*>) { + throw IllegalArgumentException("Can not use $other for addition") + } + + if (other.columns != 4 || other.rows != 4) { + throw IllegalArgumentException("Concrete Matrix4f can only use 4x4 matrixes") + } + + val m00: Float; val m01: Float; val m02: Float; val m03: Float; + val m10: Float; val m11: Float; val m12: Float; val m13: Float; + val m20: Float; val m21: Float; val m22: Float; val m23: Float; + val m30: Float; val m31: Float; val m32: Float; val m33: Float; + + if (other is Matrix4f) { + m00 = other.m00; m01 = other.m01; m02 = other.m02; m03 = other.m03; + m10 = other.m10; m11 = other.m11; m12 = other.m12; m13 = other.m13; + m20 = other.m20; m21 = other.m21; m22 = other.m22; m23 = other.m23; + m30 = other.m30; m31 = other.m31; m32 = other.m32; m33 = other.m33; + } else { + m00 = other[0, 0]; m01 = other[0, 1]; m02 = other[0, 2]; m03 = other[0, 3]; + m10 = other[1, 0]; m11 = other[1, 1]; m12 = other[1, 2]; m13 = other[1, 3]; + m20 = other[2, 0]; m21 = other[2, 1]; m22 = other[2, 2]; m23 = other[2, 3]; + m30 = other[3, 0]; m31 = other[3, 1]; m32 = other[3, 2]; m33 = other[3, 3]; + } + + return createOrModify( + this.m00 + m00, this.m01 + m01, this.m02 + m02, this.m03 + m03, + this.m10 + m10, this.m11 + m11, this.m12 + m12, this.m13 + m13, + this.m20 + m20, this.m21 + m21, this.m22 + m22, this.m23 + m23, + this.m30 + m30, this.m31 + m31, this.m32 + m32, this.m33 + m33, + ) + } + + + override fun minus(other: IMatrix): T { + if (other !is FloatMatrix<*>) { + throw IllegalArgumentException("Can not use $other for subtraction") + } + + if (other.columns != 4 || other.rows != 4) { + throw IllegalArgumentException("Concrete Matrix4f can only use 4x4 matrices") + } + + val m00: Float; val m01: Float; val m02: Float; val m03: Float; + val m10: Float; val m11: Float; val m12: Float; val m13: Float; + val m20: Float; val m21: Float; val m22: Float; val m23: Float; + val m30: Float; val m31: Float; val m32: Float; val m33: Float; + + if (other is Matrix4f) { + m00 = other.m00; m01 = other.m01; m02 = other.m02; m03 = other.m03; + m10 = other.m10; m11 = other.m11; m12 = other.m12; m13 = other.m13; + m20 = other.m20; m21 = other.m21; m22 = other.m22; m23 = other.m23; + m30 = other.m30; m31 = other.m31; m32 = other.m32; m33 = other.m33; + } else { + m00 = other[0, 0]; m01 = other[0, 1]; m02 = other[0, 2]; m03 = other[0, 3]; + m10 = other[1, 0]; m11 = other[1, 1]; m12 = other[1, 2]; m13 = other[1, 3]; + m20 = other[2, 0]; m21 = other[2, 1]; m22 = other[2, 2]; m23 = other[2, 3]; + m30 = other[3, 0]; m31 = other[3, 1]; m32 = other[3, 2]; m33 = other[3, 3]; + } + + return createOrModify( + this.m00 - m00, this.m01 - m01, this.m02 - m02, this.m03 - m03, + this.m10 - m10, this.m11 - m11, this.m12 - m12, this.m13 - m13, + this.m20 - m20, this.m21 - m21, this.m22 - m22, this.m23 - m23, + this.m30 - m30, this.m31 - m31, this.m32 - m32, this.m33 - m33, + ) + } + + override fun plus(other: Float): T { + return createOrModify( + this.m00 + other, this.m01 + other, this.m02 + other, this.m03 + other, + this.m10 + other, this.m11 + other, this.m12 + other, this.m13 + other, + this.m20 + other, this.m21 + other, this.m22 + other, this.m23 + other, + this.m30 + other, this.m31 + other, this.m32 + other, this.m33 + other, + ) + } + + override fun minus(other: Float): T { + return createOrModify( + this.m00 - other, this.m01 - other, this.m02 - other, this.m03 - other, + this.m10 - other, this.m11 - other, this.m12 - other, this.m13 - other, + this.m20 - other, this.m21 - other, this.m22 - other, this.m23 - other, + this.m30 - other, this.m31 - other, this.m32 - other, this.m33 - other, + ) + } + + override fun times(other: Float): T { + return createOrModify( + this.m00 * other, this.m01 * other, this.m02 * other, this.m03 * other, + this.m10 * other, this.m11 * other, this.m12 * other, this.m13 * other, + this.m20 * other, this.m21 * other, this.m22 * other, this.m23 * other, + this.m30 * other, this.m31 * other, this.m32 * other, this.m33 * other, + ) + } + + override fun scale(x: Float, y: Float, z: Float, w: Float): T { + return createOrModify( + this.m00 * x, this.m01, this.m02, this.m03, + this.m10, this.m11 * y, this.m12, this.m13, + this.m20, this.m21, this.m22 * z, this.m23, + this.m30, this.m31, this.m32, this.m33 * w, + ) + } + + override fun translate(x: Float, y: Float, z: Float): T { + return createOrModify( + this.m00, this.m01, this.m02, this.m03 + x, + this.m10, this.m11, this.m12, this.m13 + y, + this.m20, this.m21, this.m22, this.m23 + z, + this.m30, this.m31, this.m32, this.m33, + ) + } + + override fun div(other: Float): T { + return createOrModify( + this.m00 / other, this.m01 / other, this.m02 / other, this.m03 / other, + this.m10 / other, this.m11 / other, this.m12 / other, this.m13 / other, + this.m20 / other, this.m21 / other, this.m22 / other, this.m23 / other, + this.m30 / other, this.m31 / other, this.m32 / other, this.m33 / other, + ) + } + + override fun times(other: IMatrix): T { + if (other !is FloatMatrix<*>) { + throw IllegalArgumentException("Can not use $other for multiplication") + } + + if (other.columns != 4 || other.rows != 4) { + throw IllegalArgumentException("Concrete Matrix4f can only use 4x4 matrices") + } + + val m00: Float; val m01: Float; val m02: Float; val m03: Float; + val m10: Float; val m11: Float; val m12: Float; val m13: Float; + val m20: Float; val m21: Float; val m22: Float; val m23: Float; + val m30: Float; val m31: Float; val m32: Float; val m33: Float; + + if (other is Matrix4f) { + m00 = other.m00; m01 = other.m01; m02 = other.m02; m03 = other.m03; + m10 = other.m10; m11 = other.m11; m12 = other.m12; m13 = other.m13; + m20 = other.m20; m21 = other.m21; m22 = other.m22; m23 = other.m23; + m30 = other.m30; m31 = other.m31; m32 = other.m32; m33 = other.m33; + } else { + m00 = other[0, 0]; m01 = other[0, 1]; m02 = other[0, 2]; m03 = other[0, 3]; + m10 = other[1, 0]; m11 = other[1, 1]; m12 = other[1, 2]; m13 = other[1, 3]; + m20 = other[2, 0]; m21 = other[2, 1]; m22 = other[2, 2]; m23 = other[2, 3]; + m30 = other[3, 0]; m31 = other[3, 1]; m32 = other[3, 2]; m33 = other[3, 3]; + } + + // первый столбец + val newm00 = + this.m00 * m00 + + this.m01 * m10 + + this.m02 * m20 + + this.m03 * m30 + + val newm10 = + this.m10 * m00 + + this.m11 * m10 + + this.m12 * m20 + + this.m13 * m30 + + val newm20 = + this.m20 * m00 + + this.m21 * m10 + + this.m22 * m20 + + this.m23 * m30 + + val newm30 = + this.m30 * m00 + + this.m31 * m10 + + this.m32 * m20 + + this.m33 * m30 + + // второй столбец + val newm01 = + this.m00 * m01 + + this.m01 * m11 + + this.m02 * m21 + + this.m03 * m31 + + val newm11 = + this.m10 * m01 + + this.m11 * m11 + + this.m12 * m21 + + this.m13 * m31 + + val newm21 = + this.m20 * m01 + + this.m21 * m11 + + this.m22 * m21 + + this.m23 * m31 + + val newm31 = + this.m30 * m01 + + this.m31 * m11 + + this.m32 * m21 + + this.m33 * m31 + + // третий столбец + val newm02 = + this.m00 * m02 + + this.m01 * m12 + + this.m02 * m22 + + this.m03 * m32 + + val newm12 = + this.m10 * m02 + + this.m11 * m12 + + this.m12 * m22 + + this.m13 * m32 + + val newm22 = + this.m20 * m02 + + this.m21 * m12 + + this.m22 * m22 + + this.m23 * m32 + + val newm32 = + this.m30 * m02 + + this.m31 * m12 + + this.m32 * m22 + + this.m33 * m32 + + // четвёртый столбец + val newm03 = + this.m00 * m03 + + this.m01 * m13 + + this.m02 * m23 + + this.m03 * m33 + + val newm13 = + this.m10 * m03 + + this.m11 * m13 + + this.m12 * m23 + + this.m13 * m33 + + val newm23 = + this.m20 * m03 + + this.m21 * m13 + + this.m22 * m23 + + this.m23 * m33 + + val newm33 = + this.m30 * m03 + + this.m31 * m13 + + this.m32 * m23 + + this.m33 * m33 + + return createOrModify( + newm00, newm01, newm02, newm03, + newm10, newm11, newm12, newm13, + newm20, newm21, newm22, newm23, + newm30, newm31, newm32, newm33, + ) + } + + override fun translateWithScale(x: Float, y: Float, z: Float): T { + return createOrModify( + m00, m01, m02, m03 + x * m00 + y * m01 + z * m02, + m10, m11, m12, m13 + x * m10 + y * m11 + z * m12, + m20, m21, m22, m23 + x * m20 + y * m21 + z * m22, + m30, m31, m32, m33, + ) + } +} + +data class Matrix4f( + override val m00: Float = 1f, override val m01: Float = 0f, override val m02: Float = 0f, override val m03: Float = 0f, + override val m10: Float = 0f, override val m11: Float = 1f, override val m12: Float = 0f, override val m13: Float = 0f, + override val m20: Float = 0f, override val m21: Float = 0f, override val m22: Float = 1f, override val m23: Float = 0f, + override val m30: Float = 0f, override val m31: Float = 0f, override val m32: Float = 0f, override val m33: Float = 1f, +) : AbstractMatrix4f() { + override fun createOrModify( + m00: Float, m01: Float, m02: Float, m03: Float, + m10: Float, m11: Float, m12: Float, m13: Float, + m20: Float, m21: Float, m22: Float, m23: Float, + m30: Float, m31: Float, m32: Float, m33: Float, + ): Matrix4f { + return Matrix4f( + m00 = m00, m01 = m01, m02 = m02, m03 = m03, + m10 = m10, m11 = m11, m12 = m12, m13 = m13, + m20 = m20, m21 = m21, m22 = m22, m23 = m23, + m30 = m30, m31 = m31, m32 = m32, m33 = m33, + ) + } + + fun toMutableMatrix(): MutableMatrix4f { + return MutableMatrix4f( + m00 = m00, m01 = m01, m02 = m02, m03 = m03, + m10 = m10, m11 = m11, m12 = m12, m13 = m13, + m20 = m20, m21 = m21, m22 = m22, m23 = m23, + m30 = m30, m31 = m31, m32 = m32, m33 = m33, + ) + } + + companion object { + val IDENTITY = Matrix4f() + + val SCREEN_FLIP = IDENTITY.let { + return@let it * Vector3f.FORWARD.rotateAroundThis(-PI / 2) + } + + /** + * Возвращает матрицу ортографической проекции, с ИНВЕНТИРОВАННОЙ y координатой, и с добавлением 2f + * + * Т.е. любое значение компоненты вектора y будет иметь противоположный знак после перемножения на данную матрицу + * + * Смысл данного изменения знака в преобразовании экранных координат OpenGL к вменяемому виду. Многие примеры указывают Z как отрицательную компоненту, + * что так же "убирает" это недоумение, только вот у нас Z это глубина, а не высота + * + * y = 0 будет соответствовать верхнему левому углу окна + */ + fun ortho(left: Float, right: Float, bottom: Float, top: Float, zNear: Float, zFar: Float): Matrix4f { + return Matrix4f( + m00 = 2f / (right - left), + m11 = -2f / (top - bottom), + m22 = 2f / (zFar - zNear), + m03 = -(right + left) / (right - left), + m13 = -(top + bottom) / (top - bottom) + 2f, + m23 = -(zFar + zNear) / (zFar - zNear) + ) + } + + /** + * Возвращает матрицу ортографической проекции, без инвентирования + * + * y = 0 будет соответствовать нижнему левому углу окна + */ + fun orthoDirect(left: Float, right: Float, bottom: Float, top: Float, zNear: Float, zFar: Float): Matrix4f { + return Matrix4f( + m00 = 2f / (right - left), + m11 = 2f / (top - bottom), + m22 = 2f / (zFar - zNear), + m03 = -(right + left) / (right - left), + m13 = -(top + bottom) / (top - bottom), + m23 = -(zFar + zNear) / (zFar - zNear) + ) + } + + fun perspective(fov: Float, zFar: Float, zNear: Float): Matrix4f { + val scale = (1.0 / (tan(Math.toRadians(fov.toDouble()) / 2.0))).toFloat() + val r = zFar - zNear + + return Matrix4f( + m00 = scale, + m11 = scale, + m22 = -zFar / r, + m23 = -1f, + m32 = -zFar * zNear / r, + ) + } + } +} + +data class MutableMatrix4f( + override var m00: Float = 1f, override var m01: Float = 0f, override var m02: Float = 0f, override var m03: Float = 0f, + override var m10: Float = 0f, override var m11: Float = 1f, override var m12: Float = 0f, override var m13: Float = 0f, + override var m20: Float = 0f, override var m21: Float = 0f, override var m22: Float = 1f, override var m23: Float = 0f, + override var m30: Float = 0f, override var m31: Float = 0f, override var m32: Float = 0f, override var m33: Float = 1f, +) : AbstractMatrix4f() { + override fun createOrModify( + m00: Float, m01: Float, m02: Float, m03: Float, + m10: Float, m11: Float, m12: Float, m13: Float, + m20: Float, m21: Float, m22: Float, m23: Float, + m30: Float, m31: Float, m32: Float, m33: Float, + ): MutableMatrix4f { + this.m00 = m00; this.m01 = m01; this.m02 = m02; this.m03 = m03 + this.m10 = m10; this.m11 = m11; this.m12 = m12; this.m13 = m13 + this.m20 = m20; this.m21 = m21; this.m22 = m22; this.m23 = m23 + this.m30 = m30; this.m31 = m31; this.m32 = m32; this.m33 = m33 + + return this + } + + fun set(row: Int, column: Int, value: Float) { + when (column) { + 0 -> when (row) { + 0 -> m00 = value + 1 -> m10 = value + 2 -> m20 = value + 3 -> m30 = value + else -> throw IndexOutOfBoundsException("Row: $row") + } + + 1 -> when (row) { + 0 -> m01 = value + 1 -> m11 = value + 2 -> m21 = value + 3 -> m31 = value + else -> throw IndexOutOfBoundsException("Row: $row") + } + + 2 -> when (row) { + 0 -> m02 = value + 1 -> m12 = value + 2 -> m22 = value + 3 -> m32 = value + else -> throw IndexOutOfBoundsException("Row: $row") + } + + 3 -> when (row) { + 0 -> m03 = value + 1 -> m13 = value + 2 -> m23 = value + 3 -> m33 = value + else -> throw IndexOutOfBoundsException("Row: $row") + } + + else -> throw IndexOutOfBoundsException("Column: $column") + } + } +} + +abstract class AbstractMatrix3f> : FloatMatrix { + abstract val m00: Float; abstract val m01: Float; abstract val m02: Float + abstract val m10: Float; abstract val m11: Float; abstract val m12: Float + abstract val m20: Float; abstract val m21: Float; abstract val m22: Float + + override val columns: Int + get() = 3 + + override val rows: Int + get() = 3 + + override fun get(row: Int, column: Int): Float { + return when (column) { + 0 -> when (row) { + 0 -> m00 + 1 -> m10 + 2 -> m20 + else -> throw IndexOutOfBoundsException("Row: $row") + } + + 1 -> when (row) { + 0 -> m01 + 1 -> m11 + 2 -> m21 + else -> throw IndexOutOfBoundsException("Row: $row") + } + + 2 -> when (row) { + 0 -> m02 + 1 -> m12 + 2 -> m22 + else -> throw IndexOutOfBoundsException("Row: $row") + } + + else -> throw IndexOutOfBoundsException("Column: $column") + } + } + + protected abstract fun createOrModify( + m00: Float, m01: Float, m02: Float, + m10: Float, m11: Float, m12: Float, + m20: Float, m21: Float, m22: Float, + ): T + + override fun plus(other: IMatrix): T { + if (other !is FloatMatrix<*>) { + throw IllegalArgumentException("Can not use $other for addition") + } + + if (other.columns != 3 || other.rows != 3) { + throw IllegalArgumentException("Concrete Matrix3f can only use 3x3 matrices") + } + + val m00: Float; val m01: Float; val m02: Float; + val m10: Float; val m11: Float; val m12: Float; + val m20: Float; val m21: Float; val m22: Float; + + if (other is Matrix3f) { + m00 = other.m00; m01 = other.m01; m02 = other.m02; + m10 = other.m10; m11 = other.m11; m12 = other.m12; + m20 = other.m20; m21 = other.m21; m22 = other.m22; + } else { + m00 = other[0, 0]; m01 = other[0, 1]; m02 = other[0, 2]; + m10 = other[1, 0]; m11 = other[1, 1]; m12 = other[1, 2]; + m20 = other[2, 0]; m21 = other[2, 1]; m22 = other[2, 2]; + } + + return createOrModify( + this.m00 + m00, this.m01 + m01, this.m02 + m02, + this.m10 + m10, this.m11 + m11, this.m12 + m12, + this.m20 + m20, this.m21 + m21, this.m22 + m22, + ) + } + + override fun minus(other: IMatrix): T { + if (other !is FloatMatrix<*>) { + throw IllegalArgumentException("Can not use $other for subtraction") + } + + if (other.columns != 3 || other.rows != 3) { + throw IllegalArgumentException("Concrete Matrix3f can only use 3x3 matrices") + } + + val m00: Float; val m01: Float; val m02: Float; + val m10: Float; val m11: Float; val m12: Float; + val m20: Float; val m21: Float; val m22: Float; + + if (other is Matrix3f) { + m00 = other.m00; m01 = other.m01; m02 = other.m02; + m10 = other.m10; m11 = other.m11; m12 = other.m12; + m20 = other.m20; m21 = other.m21; m22 = other.m22; + } else { + m00 = other[0, 0]; m01 = other[0, 1]; m02 = other[0, 2]; + m10 = other[1, 0]; m11 = other[1, 1]; m12 = other[1, 2]; + m20 = other[2, 0]; m21 = other[2, 1]; m22 = other[2, 2]; + } + + return createOrModify( + this.m00 - m00, this.m01 - m01, this.m02 - m02, + this.m10 - m10, this.m11 - m11, this.m12 - m12, + this.m20 - m20, this.m21 - m21, this.m22 - m22, + ) + } + + override fun plus(other: Float): T { + return createOrModify( + this.m00 + other, this.m01 + other, this.m02 + other, + this.m10 + other, this.m11 + other, this.m12 + other, + this.m20 + other, this.m21 + other, this.m22 + other, + ) + } + + override fun minus(other: Float): T { + return createOrModify( + this.m00 - other, this.m01 - other, this.m02 - other, + this.m10 - other, this.m11 - other, this.m12 - other, + this.m20 - other, this.m21 - other, this.m22 - other, + ) + } + + override fun times(other: Float): T { + return createOrModify( + this.m00 * other, this.m01 * other, this.m02 * other, + this.m10 * other, this.m11 * other, this.m12 * other, + this.m20 * other, this.m21 * other, this.m22 * other, + ) + } + + override fun div(other: Float): T { + return createOrModify( + this.m00 / other, this.m01 / other, this.m02 / other, + this.m10 / other, this.m11 / other, this.m12 / other, + this.m20 / other, this.m21 / other, this.m22 / other, + ) + } + + override fun scale(x: Float, y: Float, z: Float, w: Float): T { + return createOrModify( + this.m00 * x, this.m01, this.m02, + this.m10, this.m11 * y, this.m12, + this.m20, this.m21, this.m22 * z, + ) + } + + override fun translate(x: Float, y: Float, z: Float): T { + return createOrModify( + this.m00, this.m01, this.m02 + x, + this.m10, this.m11, this.m12 + y, + this.m20, this.m21, this.m22, + ) + } + + override fun times(other: IMatrix): T { + if (other !is FloatMatrix<*>) { + throw IllegalArgumentException("Can not use $other for multiplication") + } + + if (other.columns != 3 || other.rows != 3) { + throw IllegalArgumentException("Concrete Matrix3f can only use 3x3 matrixes") + } + + val m00: Float; val m01: Float; val m02: Float; + val m10: Float; val m11: Float; val m12: Float; + val m20: Float; val m21: Float; val m22: Float; + + if (other is Matrix3f) { + m00 = other.m00; m01 = other.m01; m02 = other.m02; + m10 = other.m10; m11 = other.m11; m12 = other.m12; + m20 = other.m20; m21 = other.m21; m22 = other.m22; + } else { + m00 = other[0, 0]; m01 = other[0, 1]; m02 = other[0, 2]; + m10 = other[1, 0]; m11 = other[1, 1]; m12 = other[1, 2]; + m20 = other[2, 0]; m21 = other[2, 1]; m22 = other[2, 2]; + } + + // первый столбец + val newm00 = + this.m00 * m00 + + this.m01 * m10 + + this.m02 * m20 + + val newm10 = + this.m10 * m00 + + this.m11 * m10 + + this.m12 * m20 + + val newm20 = + this.m20 * m00 + + this.m21 * m10 + + this.m22 * m20 + + // второй столбец + val newm01 = + this.m00 * m01 + + this.m01 * m11 + + this.m02 * m21 + + val newm11 = + this.m10 * m01 + + this.m11 * m11 + + this.m12 * m21 + + val newm21 = + this.m20 * m01 + + this.m21 * m11 + + this.m22 * m21 + + // третий столбец + val newm02 = + this.m00 * m02 + + this.m01 * m12 + + this.m02 * m22 + + val newm12 = + this.m10 * m02 + + this.m11 * m12 + + this.m12 * m22 + + val newm22 = + this.m20 * m02 + + this.m21 * m12 + + this.m22 * m22 + + return createOrModify( + newm00, newm01, newm02, + newm10, newm11, newm12, + newm20, newm21, newm22, + ) + } + + override fun translateWithScale(x: Float, y: Float, z: Float): T { + return createOrModify( + m00, m01, m02 + x * m00 + y * m01, + m10, m11, m12 + x * m10 + y * m11, + m20, m21, m22, + ) + } +} + +data class Matrix3f( + override val m00: Float = 1f, override val m01: Float = 0f, override val m02: Float = 0f, + override val m10: Float = 0f, override val m11: Float = 1f, override val m12: Float = 0f, + override val m20: Float = 0f, override val m21: Float = 0f, override val m22: Float = 1f, +) : AbstractMatrix3f() { + override fun createOrModify( + m00: Float, m01: Float, m02: Float, + m10: Float, m11: Float, m12: Float, + m20: Float, m21: Float, m22: Float, + ): Matrix3f { + return Matrix3f( + m00 = m00, m01 = m01, m02 = m02, + m10 = m10, m11 = m11, m12 = m12, + m20 = m20, m21 = m21, m22 = m22, + ) + } + + companion object { + val IDENTITY = Matrix3f() + } +} + +data class MutableMatrix3f( + override var m00: Float = 1f, override var m01: Float = 0f, override var m02: Float = 0f, + override var m10: Float = 0f, override var m11: Float = 1f, override var m12: Float = 0f, + override var m20: Float = 0f, override var m21: Float = 0f, override var m22: Float = 1f, +) : AbstractMatrix3f() { + override fun createOrModify( + m00: Float, m01: Float, m02: Float, + m10: Float, m11: Float, m12: Float, + m20: Float, m21: Float, m22: Float, + ): MutableMatrix3f { + this.m00 = m00; this.m01 = m01; this.m02 = m02 + this.m10 = m10; this.m11 = m11; this.m12 = m12 + this.m20 = m20; this.m21 = m21; this.m22 = m22 + + return this + } + + operator fun set(row: Int, column: Int, value: Float) { + when (column) { + 0 -> when (row) { + 0 -> m00 = value + 1 -> m10 = value + 2 -> m20 = value + else -> throw IndexOutOfBoundsException("Row: $row") + } + + 1 -> when (row) { + 0 -> m01 = value + 1 -> m11 = value + 2 -> m21 = value + else -> throw IndexOutOfBoundsException("Row: $row") + } + + 2 -> when (row) { + 0 -> m02 = value + 1 -> m12 = value + 2 -> m22 = value + else -> throw IndexOutOfBoundsException("Row: $row") + } + + else -> throw IndexOutOfBoundsException("Column: $column") + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Matrix4fStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Matrix4fStack.kt new file mode 100644 index 00000000..e769283c --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Matrix4fStack.kt @@ -0,0 +1,88 @@ +package ru.dbotthepony.kstarbound.math + +class Matrix4fStack { + private val stack = ArrayDeque() + + init { + stack.add(MutableMatrix4f()) + } + + val last get() = stack.last() + + fun push(matrix4f: MutableMatrix4f = last.copy()): Matrix4fStack { + stack.add(matrix4f) + return this + } + + fun pop(): Matrix4fStack { + stack.removeLast() + return this + } + + fun clear(matrix: MutableMatrix4f = MutableMatrix4f()): Matrix4fStack { + stack.clear() + stack.add(matrix) + return this + } + + operator fun plus(other: FloatMatrix<*>): Matrix4fStack { + last.plus(other) + return this + } + + operator fun minus(other: FloatMatrix<*>): Matrix4fStack { + last.minus(other) + return this + } + + operator fun times(other: FloatMatrix<*>): Matrix4fStack { + last.times(other) + return this + } + + operator fun plus(other: Float): Matrix4fStack { + last.plus(other) + return this + } + + operator fun minus(other: Float): Matrix4fStack { + last.minus(other) + return this + } + + operator fun times(other: Float): Matrix4fStack { + last.times(other) + return this + } + + fun scale(x: Float = 1f, y: Float = 1f, z: Float = 1f, w: Float = 1f): Matrix4fStack { + last.scale(x, y, z, w) + return this + } + + fun translate(x: Float = 0f, y: Float = 0f, z: Float = 0f): Matrix4fStack { + last.translate(x, y, z) + return this + } + + fun translateWithScale(x: Float = 0f, y: Float = 0f, z: Float = 0f): Matrix4fStack { + last.translateWithScale(x, y, z) + return this + } + + fun translate(vec: Vector3f): Matrix4fStack { + last.translate(vec) + return this + } + + fun translateWithScale(vec: Vector3f): Matrix4fStack { + last.translateWithScale(vec) + return this + } + + fun replace(matrix4f: MutableMatrix4f): Matrix4fStack { + stack.removeLast() + stack.add(matrix4f) + return this + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt new file mode 100644 index 00000000..72a067c7 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt @@ -0,0 +1,162 @@ +package ru.dbotthepony.kstarbound.math + +import com.google.gson.JsonArray +import kotlin.math.cos +import kotlin.math.sin + +data class Vector2i(val x: Int, val y: Int) : IMatrixLike, IMatrixLikeGetterI { + operator fun plus(other: Vector2i) = Vector2i(x + other.x, y + other.y) + operator fun plus(other: Int) = Vector2i(x + other, y + other) + operator fun minus(other: Vector2i) = Vector2i(x - other.x, y - other.y) + operator fun minus(other: Int) = Vector2i(x - other, y - other) + operator fun times(other: Vector2i) = Vector2i(x * other.x, y * other.y) + operator fun times(other: Int) = Vector2i(x * other, y * other) + operator fun div(other: Vector2i) = Vector2i(x / other.x, y / other.y) + operator fun div(other: Int) = Vector2i(x / other, y / other) + + override val columns = 1 + override val rows = 2 + + override fun get(row: Int, column: Int): Int { + if (column != 0) { + throw IndexOutOfBoundsException("Column must be 0 ($column given)") + } + + return when (row) { + 0 -> x + 1 -> y + else -> throw IndexOutOfBoundsException("Row out of bounds: $row") + } + } + + companion object { + fun fromJson(input: JsonArray): Vector2i { + return Vector2i(input[0].asInt, input[1].asInt) + } + + val ZERO = Vector2i(0, 0) + } +} + +data class Vector3f(val x: Float = 0f, val y: Float = 0f, val z: Float = 0f) : IMatrixLikeFloat { + override val columns = 1 + override val rows = 3 + + override fun get(row: Int, column: Int): Float { + if (column != 0) { + throw IndexOutOfBoundsException("Column must be 0 ($column given)") + } + + return when (row) { + 0 -> x + 1 -> y + 2 -> z + else -> throw IndexOutOfBoundsException("Row out of bounds: $row") + } + } + + fun rotateAroundThis(rotation: Double): Matrix4f { + val c = cos(rotation).toFloat() + val s = sin(rotation).toFloat() + val cInv = 1f - c + + return Matrix4f( + m00 = c + x * x * cInv, m01 = x * y * cInv - z * s, m02 = x * z * cInv + y * s, + m10 = y * x * cInv + z * s, m11 = c + y * y * cInv, m12 = y * z * cInv - x * s, + m20 = z * x * cInv - y * s, m21 = z * y * cInv + x * s, m22 = c + z * z * cInv, + ) + } + + operator fun times(other: IMatrixLikeFloat): Vector3f { + if (other.rows >= 4 && other.columns >= 4) { + val x = this.x * other[0, 0] + + this.y * other[0, 1] + + this.z * other[0, 2] + + other[0, 3] + + val y = this.x * other[1, 0] + + this.y * other[1, 1] + + this.z * other[1, 2] + + other[1, 3] + + val z = this.x * other[2, 0] + + this.y * other[2, 1] + + this.z * other[2, 2] + + other[2, 3] + + return Vector3f(x, y, z) + } else if (other.rows >= 3 && other.columns >= 3) { + val x = this.x * other[0, 0] + + this.y * other[0, 1] + + this.z * other[0, 2] + + val y = this.x * other[1, 0] + + this.y * other[1, 1] + + this.z * other[1, 2] + + val z = this.x * other[2, 0] + + this.y * other[2, 1] + + this.z * other[2, 2] + + return Vector3f(x, y, z) + } + + throw IllegalArgumentException("Incompatible matrix provided: ${other.rows} x ${other.columns}") + } + + companion object { + val UP = Vector3f(0f, 1f, 0f) + val DOWN = Vector3f(0f, -1f, 0f) + val LEFT = Vector3f(-1f, 0f, 0f) + val RIGHT = Vector3f(1f, 0f, 0f) + val FORWARD = Vector3f(0f, 0f, 1f) + val BACKWARD = Vector3f(0f, 0f, -1f) + } +} + +data class Vector4f(val x: Float, val y: Float, val z: Float, val w: Float) : IMatrixLikeFloat { + override val columns = 1 + override val rows = 4 + + override fun get(row: Int, column: Int): Float { + if (column != 0) { + throw IndexOutOfBoundsException("Column must be 0 ($column given)") + } + + return when (row) { + 0 -> x + 1 -> y + 2 -> z + 3 -> w + else -> throw IndexOutOfBoundsException("Row out of bounds: $row") + } + } + + operator fun times(other: IMatrixLikeFloat): Vector4f { + if (other.rows >= 4 && other.columns >= 4) { + val x = this.x * other[0, 0] + + this.y * other[0, 1] + + this.z * other[0, 2] + + this.w * other[0, 3] + + val y = this.x * other[1, 0] + + this.y * other[1, 1] + + this.z * other[1, 2] + + this.w * other[1, 3] + + val z = this.x * other[2, 0] + + this.y * other[2, 1] + + this.z * other[2, 2] + + this.w * other[2, 3] + + val w = this.x * other[3, 0] + + this.y * other[3, 1] + + this.z * other[3, 2] + + this.w * other[3, 3] + + return Vector4f(x, y, z, w) + } + + throw IllegalArgumentException("Incompatible matrix provided: ${other.rows} x ${other.columns}") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/render/BakedProgramState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/render/BakedProgramState.kt new file mode 100644 index 00000000..8817369a --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/render/BakedProgramState.kt @@ -0,0 +1,92 @@ +package ru.dbotthepony.kstarbound.render + +import org.lwjgl.opengl.GL11 +import org.lwjgl.opengl.GL46.* +import ru.dbotthepony.kstarbound.gl.* +import ru.dbotthepony.kstarbound.math.FloatMatrix +import ru.dbotthepony.kstarbound.math.Matrix4f + +/** + * Служит для быстрой настройки состояния для будущей отрисовки + * + * Установка текстурных блоков, программы, самих текстур и загрузка юниформ должна осуществляться тут + * + * Класс обязан быть наследован для осмысленных результатов + * + * Ожидается, что состояние будет выставлено ПОЛНОСТЬЮ, т.е. НИКАКОЙ предыдущий код НЕ МОЖЕТ повлиять на результат выполнения + * шейдерной программы, которая связанна с этим объектом (за исключением не вызова [setTransform] внешним кодом) + */ +open class BakedProgramState( + val program: GLShaderProgram, +) { + private val transformLocation = program["_transform"] + + open fun setTransform(value: FloatMatrix<*>) { + transformLocation?.set(value) + } + + /** + * Вызывается перед началом отрисовки + */ + open fun setup() { + program.use() + } +} + +/** + * Запечённый статичный меш, позволяет быстро отрисовать меш со всеми параметрами + * с заданной матрицей трансформации + */ +class BakedStaticMesh( + val programState: BakedProgramState, + val indexCount: Int, + val vao: GLVertexArrayObject, +) : AutoCloseable { + private var onClose = {} + constructor(programState: BakedProgramState, builder: VertexBuilder) : this( + programState, + builder.indexCount, + programState.program.state.newVAO(), + ) { + val vbo = programState.program.state.newVBO() + val ebo = programState.program.state.newEBO() + + onClose = { + vbo.close() + ebo.close() + } + + vao.bind() + vbo.bind() + ebo.bind() + + builder.upload(vbo, ebo, GL_STATIC_DRAW) + builder.attributes.apply(vao, true) + + vao.unbind() + vbo.unbind() + ebo.unbind() + } + + fun render(transform: FloatMatrix<*>? = null) { + check(isValid) { "$this is no longer valid" } + programState.setup() + + if (transform != null) { + programState.setTransform(transform) + } + + vao.bind() + glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0L) + checkForGLError() + } + + var isValid = true + private set + + override fun close() { + vao.close() + onClose.invoke() + isValid = false + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/render/Camera.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/render/Camera.kt new file mode 100644 index 00000000..c4825fa7 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/render/Camera.kt @@ -0,0 +1,19 @@ +package ru.dbotthepony.kstarbound.render + +import ru.dbotthepony.kstarbound.math.Matrix4f +import ru.dbotthepony.kstarbound.math.Vector3f + +class Camera { + var pos = Vector3f(z = 400f) + var zoom = 1f + + fun matrix(): Matrix4f { + return Matrix4f.IDENTITY.scale(zoom, zoom, zoom).translate(pos) + } + + companion object { + const val MAX_ZOOM = 4f + const val MIN_ZOOM = 0.1f + const val ZOOM_STEP = 0.1f + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/render/ChunkRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/render/ChunkRenderer.kt new file mode 100644 index 00000000..5d1fbbf8 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/render/ChunkRenderer.kt @@ -0,0 +1,95 @@ +package ru.dbotthepony.kstarbound.render + +import ru.dbotthepony.kstarbound.gl.* +import ru.dbotthepony.kstarbound.math.FloatMatrix +import ru.dbotthepony.kstarbound.world.Chunk +import java.util.* +import kotlin.collections.ArrayList +import kotlin.collections.HashMap + +class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk) : AutoCloseable { + private val builders = HashMap() + private val bakedMeshes = ArrayList() + private val unloadableBakedMeshes = ArrayList() + + /** + * Тесселирует "статичную" геометрию в builders (к примеру тайлы). + * + * Может быть вызван вне рендер потока (ибо в любом случае он требует некой "стаитичности" данных в чанке) + * но только если до этого был вызыван loadRenderers() и геометрия чанка не поменялась + */ + fun tesselateStatic() { + if (state.isSameThread()) { + for (mesh in bakedMeshes) { + mesh.close() + } + + bakedMeshes.clear() + } else { + unloadableBakedMeshes.addAll(bakedMeshes) + bakedMeshes.clear() + } + + builders.clear() + + // TODO: Синхронизация (ибо обновления игровой логики будут в потоке вне рендер потока) + for ((pos, tile) in chunk.posToTile) { + if (tile != null) { + val renderer = state.tileRenderers.get(tile.def.materialName) + renderer.tesselate(chunk, builders, pos) + } + } + } + + /** + * Принудительно подгружает в GLStateTracker все необходимые рендереры (ибо им нужны текстуры и прочее) + * + * Вызывается перед tesselateStatic() + */ + fun loadRenderers() { + unloadUnused() + + // TODO: Синхронизация (ибо обновления игровой логики будут в потоке вне рендер потока) + for ((pos, tile) in chunk.posToTile) { + if (tile != null) { + state.tileRenderers.get(tile.def.materialName) + } + } + } + + private fun unloadUnused() { + if (unloadableBakedMeshes.size != 0) { + for (baked in unloadableBakedMeshes) { + baked.close() + } + + unloadableBakedMeshes.clear() + } + } + + fun uploadStatic() { + unloadUnused() + + for ((baked, builder) in builders) { + bakedMeshes.add(BakedStaticMesh(baked, builder)) + } + } + + fun render(transform: FloatMatrix<*> = state.matrixStack.last) { + unloadUnused() + + for (mesh in bakedMeshes) { + mesh.render(transform) + } + } + + override fun close() { + for (mesh in bakedMeshes) { + mesh.close() + } + + for (mesh in unloadableBakedMeshes) { + mesh.close() + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/render/TileRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/render/TileRenderer.kt new file mode 100644 index 00000000..2491cabc --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/render/TileRenderer.kt @@ -0,0 +1,244 @@ +package ru.dbotthepony.kstarbound.render + +import org.lwjgl.glfw.GLFW.glfwGetTime +import org.lwjgl.opengl.GL46.* +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.TileDefinition +import ru.dbotthepony.kstarbound.defs.TileRenderMatchPiece +import ru.dbotthepony.kstarbound.defs.TileRenderMatchedPiece +import ru.dbotthepony.kstarbound.defs.TileRenderPiece +import ru.dbotthepony.kstarbound.gl.* +import ru.dbotthepony.kstarbound.math.Matrix4f +import ru.dbotthepony.kstarbound.math.Vector2i +import ru.dbotthepony.kstarbound.world.CHUNK_SIZE +import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF +import ru.dbotthepony.kstarbound.world.IChunk +import kotlin.collections.HashMap + +class TileRenderers(val state: GLStateTracker) { + private val simpleBakedPrograms = HashMap() + private val tileRenderers = HashMap() + + operator fun get(tile: String): TileRenderer { + return tileRenderers.computeIfAbsent(tile) { + return@computeIfAbsent TileRenderer(state, Starbound.loadTileDefinition(tile)) + } + } + + private inner class SimpleBakedProgram(private val texture: GLTexture2D) : BakedProgramState(state.shaderVertexTexture) { + override fun setup() { + super.setup() + state.activeTexture = 0 + program["_texture"] = 0 + texture.bind() + texture.textureMagFilter = GL_NEAREST + texture.textureMinFilter = GL_NEAREST + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + if (other is SimpleBakedProgram) { + return texture == other.texture + } + + return super.equals(other) + } + + override fun hashCode(): Int { + return texture.hashCode() + } + } + + /** + * Возвращает запечённое состояние shaderVertexTexture с данной текстурой + */ + fun simpleProgram(texture: GLTexture2D): BakedProgramState { + return simpleBakedPrograms.computeIfAbsent(texture, ::SimpleBakedProgram) + } +} + +class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) { + val texture = state.loadNamedTexture(tile.render.texture).also { + it.textureMagFilter = GL_NEAREST + } + + val bakedProgramState = state.tileRenderers.simpleProgram(texture) + + private fun tesselateAt(piece: TileRenderPiece, getter: IChunk, builder: VertexBuilder, pos: Vector2i, offset: Vector2i = Vector2i.ZERO) { + val fx = pos.x.toFloat() + val fy = pos.y.toFloat() + + var a = fx + var b = fy + + var c = fx + piece.textureSize.x / BASELINE_TEXTURE_SIZE + var d = fy + piece.textureSize.y / BASELINE_TEXTURE_SIZE + + if (offset != Vector2i.ZERO) { + a += offset.x / BASELINE_TEXTURE_SIZE + + // в json файлах y указан как положительный вверх + b += offset.y / BASELINE_TEXTURE_SIZE + + c += offset.x / BASELINE_TEXTURE_SIZE + d += offset.y / BASELINE_TEXTURE_SIZE + } + + if (tile.render.variants == 0 || piece.texture != null || piece.variantStride == null) { + val (u0, v0) = texture.pixelToUV(piece.texturePosition) + val (u1, v1) = texture.pixelToUV(piece.texturePosition + piece.textureSize) + + builder.quadZ(a, b, c, d, 1f, VertexTransformers.uv(u0, v1, u1, v0)) + } else { + val variant = (getter.randomDoubleFor(pos) * tile.render.variants).toInt() + + val (u0, v0) = texture.pixelToUV(piece.texturePosition + piece.variantStride * variant) + val (u1, v1) = texture.pixelToUV(piece.texturePosition + piece.textureSize + piece.variantStride * variant) + + builder.quadZ(a, b, c, d, 1f, VertexTransformers.uv(u0, v1, u1, v0)) + } + } + + private fun tesselatePiece(matchPiece: TileRenderMatchPiece, getter: IChunk, builders: MutableMap, pos: Vector2i, thisBuilder: VertexBuilder) { + if (matchPiece.test(getter, tile, pos)) { + for (renderPiece in matchPiece.pieces) { + if (renderPiece.piece.texture != null) { + tesselateAt(renderPiece.piece, getter, builders.computeIfAbsent(state.tileRenderers.simpleProgram(state.loadNamedTexture(renderPiece.piece.texture))) { + return@computeIfAbsent VertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS) + }, pos, renderPiece.offset) + } else { + tesselateAt(renderPiece.piece, getter, thisBuilder, pos, renderPiece.offset) + } + } + + for (subPiece in matchPiece.subMatches) { + tesselatePiece(subPiece, getter, builders, pos, thisBuilder) + } + } + } + + /** + * Тесселирует тайлы в заданной позиции в нужном билдере + * + * [getter] Нужен для получения информации о ближайших блоках + * + * [builders] содержит текущие программы и их билдеры + * + * Тесселирует тайлы в границы -1f .. CHUNK_SIZEf + 1f на основе [pos] + */ + fun tesselate(getter: IChunk, builders: MutableMap, pos: Vector2i) { + // если у нас нет renderTemplate + // то мы просто не можем его отрисовать + tile.render.renderTemplate ?: return + + val builder = builders.computeIfAbsent(bakedProgramState) { + return@computeIfAbsent VertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS) + } + + for ((_, matcher) in tile.render.renderTemplate.matches) { + for (matchPiece in matcher.pieces) { + tesselatePiece(matchPiece, getter, builders, pos, builder) + } + } + + /* + val fx = pos.x.toFloat() + val fy = pos.y.toFloat() + + val a = fx + val b = fy + + val c = fx + 1f + val d = fy + 1f + + val piece = tile.render.renderTemplate.pieces[tile.render.renderTemplate.representativePiece]!! + + if (tile.render.variants == 0) { + val (u0, v0) = texture.pixelToUV(piece.texturePosition) + val (u1, v1) = texture.pixelToUV(piece.texturePosition + piece.textureSize) + + builder.quadZ(b, a, d, c, 1f, VertexTransformers.uv(u0, v0, u1, v1)) + } else { + val variant = (getter.randomDoubleFor(pos) * tile.render.variants).toInt() + + val (u0, v0) = texture.pixelToUV(piece.texturePosition + piece.variantStride!! * variant) + val (u1, v1) = texture.pixelToUV(piece.texturePosition + piece.textureSize + piece.variantStride * variant) + + builder.quadZ(b, a, d, c, 1f, VertexTransformers.uv(u0, v0, u1, v1)) + } + */ + } + + fun renderPiece() { + val vao = state.newVAO() + val vbo = state.newVBO() + val ebo = state.newEBO() + + vao.bind() + vbo.bind() + + val base = tile.render.renderTemplate!!.pieces["base"]!! + + val builder = GLFlatAttributeListBuilder.VERTEX_TEXTURE.vertexBuilder(VertexType.QUADS) + + run { + val pos1 = base.texturePosition + base.variantStride!! * (glfwGetTime() % 15).toInt() + val pos2 = base.texturePosition + base.textureSize + base.variantStride * (glfwGetTime() % 15).toInt() + + val (u0, v0) = texture.pixelToUV(pos1) + val (u1, v1) = texture.pixelToUV(pos2) + + builder.quadZ(-1f, -1f, 1f, 1f, 0f, VertexTransformers.uv(u0, v0, u1, v1)) + } + + run { + val pos1 = base.texturePosition + base.variantStride!! * ((glfwGetTime() + 1) % 15).toInt() + val pos2 = base.texturePosition + base.textureSize + base.variantStride * ((glfwGetTime() + 1) % 15).toInt() + + val (u0, v0) = texture.pixelToUV(pos1) + val (u1, v1) = texture.pixelToUV(pos2) + + builder.quadZ(-3f, -1f, -1f, 1f, 0f, VertexTransformers.uv(u0, v0, u1, v1)) + } + + run { + val pos1 = base.texturePosition + base.variantStride!! * ((glfwGetTime() + 2) % 15).toInt() + val pos2 = base.texturePosition + base.textureSize + base.variantStride * ((glfwGetTime() + 2) % 15).toInt() + + val (u0, v0) = texture.pixelToUV(pos1) + val (u1, v1) = texture.pixelToUV(pos2) + + builder.quadZ(3f, -1f, 1f, 1f, 0f, VertexTransformers.uv(u0, v0, u1, v1)) + } + + builder.upload(vbo, ebo, GL_STREAM_DRAW) + GLFlatAttributeListBuilder.VERTEX_TEXTURE.apply(vao, enable = true) + + state.shaderVertexTexture.use() + + state.activeTexture = 0 + texture.bind() + + state.shaderVertexTexture["_texture"] = 0 + state.shaderVertexTexture["_transform"] = Matrix4f.IDENTITY.scale(0.5f, 0.5f) + + texture.textureMagFilter = GL_NEAREST + texture.textureMinFilter = GL_NEAREST_MIPMAP_NEAREST + + vbo.bind() + ebo.bind() + glDrawElements(GL_TRIANGLES, builder.indexCount, GL_UNSIGNED_INT, 0L) + checkForGLError() + + vao.close() + vbo.close() + ebo.close() + } + + companion object { + const val BASELINE_TEXTURE_SIZE = 8f + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Color.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Color.kt new file mode 100644 index 00000000..c02c19dd --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Color.kt @@ -0,0 +1,11 @@ +package ru.dbotthepony.kstarbound.util + +import com.google.gson.JsonArray + +data class Color(val red: Float, val green: Float, val blue: Float, val alpha: Float = 1f) { + constructor(input: JsonArray) : this(input[0].asFloat / 255f, input[1].asFloat / 255f, input[2].asFloat / 255f, if (input.size() >= 4) input[3].asFloat / 255f else 1f) + + companion object { + val WHITE = Color(1f, 1f, 1f) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt new file mode 100644 index 00000000..9a2d7bc7 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -0,0 +1,161 @@ +package ru.dbotthepony.kstarbound.world + +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.TileDefinition +import ru.dbotthepony.kstarbound.math.Vector2i + +data class ChunkTile(val def: TileDefinition) { + companion object { + } +} + +interface IChunk { + val pos: ChunkPos + + /** + * Возвращает тайл по ОТНОСИТЕЛЬНЫМ координатам внутри чанка + */ + operator fun get(x: Int, y: Int): ChunkTile? + operator fun get(pos: Vector2i) = get(pos.x, pos.y) + + /** + * Относительная проверка находится ли координата вне границ чагка + */ + fun isOutside(x: Int, y: Int): Boolean { + return x !in 0 until CHUNK_SIZE || y !in 0 until CHUNK_SIZE + } + + /** + * Возвращает псевдослучайное Long для заданной позиции + * + * Для использования в рендерах и прочих вещах, которым нужно стабильное число на основе своей позиции + */ + fun randomLongFor(x: Int, y: Int): Long { + var long = x * 738548L + y * 2191293543L + long = long xor 8339437585692L + long = (long ushr 4) or (long shl 52) + long *= 7848344324L + long = (long ushr 12) or (long shl 44) + return long + } + + + /** + * Возвращает псевдослучайное нормализированное Double для заданной позиции + * + * Для использования в рендерах и прочих вещах, которым нужно стабильное число на основе своей позиции + */ + fun randomDoubleFor(x: Int, y: Int): Double { + return (randomLongFor(x, y) / 9.223372036854776E18) / 2.0 + 0.5 + } + + + /** + * Возвращает псевдослучайное Long для заданной позиции + * + * Для использования в рендерах и прочих вещах, которым нужно стабильное число на основе своей позиции + */ + fun randomLongFor(pos: Vector2i) = randomLongFor(pos.x, pos.y) + + /** + * Возвращает псевдослучайное нормализированное Double для заданной позиции + * + * Для использования в рендерах и прочих вещах, которым нужно стабильное число на основе своей позиции + */ + fun randomDoubleFor(pos: Vector2i) = randomDoubleFor(pos.x, pos.y) + + /** + * Возвращает итератор пар + * + * Вектор имеет ОТНОСИТЕЛЬНЫЕ значения внутри самого чанка + */ + val posToTile: Iterator> get() { + return object : Iterator> { + private var x = 0 + private var y = 0 + + private fun idx() = x + CHUNK_SIZE * y + + override fun hasNext(): Boolean { + return idx() < CHUNK_SIZE * CHUNK_SIZE + } + + override fun next(): Pair { + if (!hasNext()) { + throw IllegalStateException("Already iterated everything!") + } + + val tile = this@IChunk[x, y] + val pos = Vector2i(x, y) + + x++ + + if (x >= CHUNK_SIZE) { + y++ + x = 0 + } + + return pos to tile + } + } + } +} + +interface IChunkSetter { + /** + * Устанавливает тайл по ОТНОСИТЕЛЬНЫМ координатам внутри чанка + */ + operator fun set(x: Int, y: Int, tile: ChunkTile?) +} + +interface IMutableChunk : IChunk, IChunkSetter + +const val CHUNK_SHIFT = 6 +const val CHUNK_SIZE = 1 shl CHUNK_SHIFT // 64 +const val CHUNK_SIZE_FF = CHUNK_SIZE - 1 + +data class ChunkPos(val x: Int, val y: Int) { + constructor(pos: Vector2i) : this(pos.x shr CHUNK_SHIFT, pos.y shr CHUNK_SHIFT) + + val firstBlock get() = Vector2i(x shl CHUNK_SHIFT, y shl CHUNK_SHIFT) + val lastBlock get() = Vector2i(((x + 1) shl CHUNK_SHIFT) - 1, ((y + 1) shl CHUNK_SHIFT) - 1) +} + +class Chunk(val world: World, override val pos: ChunkPos) : IMutableChunk { + /** + * Хранит тайлы как x + y * CHUNK_SIZE + */ + val tiles = arrayOfNulls(CHUNK_SIZE * CHUNK_SIZE) + + override operator fun get(x: Int, y: Int): ChunkTile? { + if (isOutside(x, y)) + return null + + return tiles[x or (y shl CHUNK_SHIFT)] + } + + override operator fun set(x: Int, y: Int, tile: ChunkTile?) { + if (isOutside(x, y)) + throw IndexOutOfBoundsException("Trying to set tile ${tile?.def?.materialName} at $x $y, but that is outside of chunk's range") + + tiles[x or (y shl CHUNK_SHIFT)] = tile + } + + override fun randomLongFor(x: Int, y: Int): Long { + return super.randomLongFor(x, y) xor world.seed + } + + companion object { + val EMPTY = object : IMutableChunk { + override val pos = ChunkPos(0, 0) + + override fun get(x: Int, y: Int): ChunkTile? { + return null + } + + override fun set(x: Int, y: Int, tile: ChunkTile?) { + + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt new file mode 100644 index 00000000..e8bfe3dd --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -0,0 +1,43 @@ +package ru.dbotthepony.kstarbound.world + +import ru.dbotthepony.kstarbound.math.Vector2i + +class World(val seed: Long = 0L) { + val chunkMap = ArrayList>() + + fun getChunk(pos: ChunkPos): Chunk? { + for ((k, v) in chunkMap) { + if (k == pos) { + return v + } + } + + return null + } + + fun getChunk(pos: Vector2i) = getChunk(ChunkPos(pos)) + + fun getOrMakeChunk(pos: ChunkPos): Chunk { + for ((k, v) in chunkMap) { + if (k == pos) { + return v + } + } + + val chunk = Chunk(this, pos) + chunkMap.add(pos to chunk) + return chunk + } + + fun getOrMakeChunk(pos: Vector2i) = getOrMakeChunk(ChunkPos(pos)) + + fun getTile(pos: Vector2i): ChunkTile? { + return getChunk(pos)?.get(pos.x, pos.y) + } + + fun setTile(pos: Vector2i, tile: ChunkTile?): Chunk { + val chunk = getOrMakeChunk(pos) + chunk[pos.x and CHUNK_SIZE_FF, pos.y and CHUNK_SIZE_FF] = tile + return chunk + } +} diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 00000000..202f56cd --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/shaders/f_texture.glsl b/src/main/resources/shaders/f_texture.glsl new file mode 100644 index 00000000..fd8ec725 --- /dev/null +++ b/src/main/resources/shaders/f_texture.glsl @@ -0,0 +1,13 @@ + +#version 460 + +uniform sampler2D _texture; + +in vec2 _uv_out; + +out vec4 _color_out; + +void main() { + _color_out = texture(_texture, _uv_out); + //_color_out = vec4(1.0, 1.0, 1.0, 1.0); +} diff --git a/src/main/resources/shaders/tile_fragment.glsl b/src/main/resources/shaders/tile_fragment.glsl new file mode 100644 index 00000000..56e0526e --- /dev/null +++ b/src/main/resources/shaders/tile_fragment.glsl @@ -0,0 +1,13 @@ + +#version 460 + +out vec4 FragColor; +in vec3 ourColor; +in vec2 TexCoord; + +uniform vec3 globalColor; +uniform sampler2D ourTexture; + +void main() { + FragColor = texture(ourTexture, TexCoord); +} diff --git a/src/main/resources/shaders/tile_vertex.glsl b/src/main/resources/shaders/tile_vertex.glsl new file mode 100644 index 00000000..4711ada5 --- /dev/null +++ b/src/main/resources/shaders/tile_vertex.glsl @@ -0,0 +1,14 @@ + +#version 460 + +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec3 aColor; +layout (location = 2) in vec2 aTexCoord; +out vec3 ourColor; +out vec2 TexCoord; + +void main() { + gl_Position = vec4(aPos, 1.0); + ourColor = aColor; + TexCoord = aTexCoord; +} diff --git a/src/main/resources/shaders/v_vertex_texture.glsl b/src/main/resources/shaders/v_vertex_texture.glsl new file mode 100644 index 00000000..e1ce7ea4 --- /dev/null +++ b/src/main/resources/shaders/v_vertex_texture.glsl @@ -0,0 +1,14 @@ + +#version 460 + +layout (location = 0) in vec3 _pos; +layout (location = 1) in vec2 _uv_in; + +out vec2 _uv_out; + +uniform mat4 _transform; + +void main() { + _uv_out = _uv_in; + gl_Position = _transform * vec4(_pos, 1.0); +} diff --git a/src/test/kotlin/ru/dbotthepony/kstarbound/test/MatrixTest.kt b/src/test/kotlin/ru/dbotthepony/kstarbound/test/MatrixTest.kt new file mode 100644 index 00000000..fc27ccc1 --- /dev/null +++ b/src/test/kotlin/ru/dbotthepony/kstarbound/test/MatrixTest.kt @@ -0,0 +1,31 @@ +package ru.dbotthepony.kstarbound.test + +import jdk.jfr.Description +import org.junit.jupiter.api.Test +import ru.dbotthepony.kstarbound.math.Matrix3f + +object MatrixTest { + @Test + @Description("Matrix test") + fun test() { + val a = Matrix3f( + 4f, 2f, 0f, + 0f, 8f, 1f, + 0f, 1f, 0f + ) + + val b = Matrix3f( + 4f, 2f, 1f, + 2f, 0f, 4f, + 9f, 4f, 2f + ) + + val c = Matrix3f( + 20f, 8f, 12f, + 25f, 4f, 34f, + 2f, 0f, 4f + ) + + require(a * b == c) { "${a * b} != $c" } + } +}