Оно существует

This commit is contained in:
DBotThePony 2022-02-02 22:48:12 +07:00
commit 8a13a99713
Signed by: DBot
GPG Key ID: DCC23B5715498507
39 changed files with 4073 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
build/
unpacked_assets/
.gradle/
.idea/

74
build.gradle.kts Normal file
View File

@ -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>("test") {
useJUnitPlatform()
}

1
gradle.properties Normal file
View File

@ -0,0 +1 @@
kotlin.code.style=official

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

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

185
gradlew vendored Normal file
View File

@ -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" "$@"

89
gradlew.bat vendored Normal file
View File

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

2
settings.gradle.kts Normal file
View File

@ -0,0 +1,2 @@
rootProject.name = "KStarBound"

View File

@ -0,0 +1,13 @@
package ru.dbotthepony.kstarbound
class GameRegistry<T> {
private val table = HashMap<String, T>()
val access: Map<String, T> by table
var frozen = false
private set
fun freeze() {
frozen = true
}
}

View File

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

View File

@ -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<String, TileDefinition>()
val world = World()
private val _filepath = ArrayList<File>()
val filepath = object : List<File> 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)
}
}
}

View File

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

View File

@ -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<String, String>,
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<Pair<String, String>>()
var footstepSound: String? = null
var health = 0
var category = "generic"
val render = TileRenderDefinitionBuilder()
fun build(directory: String? = null): TileDefinition {
return TileDefinition(
racialDescription = ImmutableMap.builder<String, String>().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<String, Any>) {
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<String, Any>): 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<String, Any>()
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<String, Any>) : 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<String, Any>) : 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<RenderRule>
) {
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<RenderRule>(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<String, TileRenderPiece>): 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<String, TileRenderRule>): 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<TileRenderMatchedPiece>,
val matchAllPoints: List<TileRenderMatchPositioned>,
val subMatches: List<TileRenderMatchPiece>
) {
/**
* Возвращает, сработали ли ВСЕ [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<String, TileRenderPiece>, rulePieces: Map<String, TileRenderRule>): TileRenderMatchPiece {
val pieces = input["pieces"]?.asJsonArray?.let {
val list = ArrayList<TileRenderMatchedPiece>()
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<TileRenderMatchPositioned>()
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<TileRenderMatchPiece>()
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<TileRenderMatchPiece>,
) {
companion object {
fun fromJson(input: JsonArray, tilePieces: Map<String, TileRenderPiece>, rulePieces: Map<String, TileRenderRule>): TileRenderMatch {
val name = input[0].asString
val pieces = ArrayList<TileRenderMatchPiece>()
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<String, TileRenderPiece>,
val rules: Map<String, TileRenderRule>,
val matches: Map<String, TileRenderMatch>,
) {
companion object {
val map = HashMap<String, TileRenderTemplate>()
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<String, TileRenderPiece>()
val rules = HashMap<String, TileRenderRule>()
val matches = HashMap<String, TileRenderMatch>()
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,
)
}
}

View File

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

View File

@ -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<AttributeListPosition>
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<AttributeListPosition>()
var offset = 0L
var stride = 0
for (i in builder.attributes) {
stride += i.second.byteSize
}
this.stride = stride
// val counter = mutableMapOf<Int, Int>()
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<Pair<String, GLType>>()
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
}
}

View File

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

View File

@ -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<GLShader>()
val access = object : Collection<GLShader> 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)
}
}

View File

@ -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 = "<unknown>") = GLTexture2D(this, name)
private val named2DTextures = HashMap<String, GLTexture2D>()
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()
}

View File

@ -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 = "<unknown>") {
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()
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<T : FloatMatrix<T>> : 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<T : MutableFloatMatrix<T>> : FloatMatrix<T> {
operator fun set(row: Int, column: Int, value: Float)
}
abstract class AbstractMatrix4f<T : AbstractMatrix4f<T>> : FloatMatrix<T> {
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<Matrix4f>() {
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<MutableMatrix4f>() {
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<T : AbstractMatrix3f<T>> : FloatMatrix<T> {
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<Matrix3f>() {
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<MutableMatrix3f>() {
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")
}
}
}

View File

@ -0,0 +1,88 @@
package ru.dbotthepony.kstarbound.math
class Matrix4fStack {
private val stack = ArrayDeque<MutableMatrix4f>()
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
}
}

View File

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

View File

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

View File

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

View File

@ -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<BakedProgramState, VertexBuilder>()
private val bakedMeshes = ArrayList<BakedStaticMesh>()
private val unloadableBakedMeshes = ArrayList<BakedStaticMesh>()
/**
* Тесселирует "статичную" геометрию в 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()
}
}
}

View File

@ -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<GLTexture2D, SimpleBakedProgram>()
private val tileRenderers = HashMap<String, TileRenderer>()
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<BakedProgramState, VertexBuilder>, 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<BakedProgramState, VertexBuilder>, 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
}
}

View File

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

View File

@ -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)
/**
* Возвращает итератор пар <Vector2i, Тайл?>
*
* Вектор имеет ОТНОСИТЕЛЬНЫЕ значения внутри самого чанка
*/
val posToTile: Iterator<Pair<Vector2i, ChunkTile?>> get() {
return object : Iterator<Pair<Vector2i, ChunkTile?>> {
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<Vector2i, ChunkTile?> {
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<ChunkTile>(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?) {
}
}
}
}

View File

@ -0,0 +1,43 @@
package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.math.Vector2i
class World(val seed: Long = 0L) {
val chunkMap = ArrayList<Pair<ChunkPos, Chunk>>()
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
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level [%t] - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

View File

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

View File

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

View File

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

View File

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

View File

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