From 5fe7668fe5d2312c2a1f6f4c1a60a05bc9e9ed08 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sun, 6 Feb 2022 01:00:40 +0700 Subject: [PATCH] =?UTF-8?q?=D0=9D=D1=83=20=D0=BC=D1=8B=20=D0=B8=D0=B4?= =?UTF-8?q?=D0=B5=D0=BC=20=D0=B4=D0=B0=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=B5=D0=B7=D0=BA=D0=B0=20=D0=B3=D0=B5=D0=BE=D0=BC?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D0=B8=D0=B8=20=D0=BC=D0=B8=D1=80=D0=B0=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=20=D0=BA=D0=BE=D0=BB=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=B8=D0=B9=20=D1=81=D1=83=D1=89=D0=BD=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 104 ++-- .../ru/dbotthepony/kstarbound/Starbound.kt | 4 +- .../kstarbound/client/ClientSettings.kt | 6 +- .../kstarbound/client/ClientWorld.kt | 27 +- .../kstarbound/client/StarboundClient.kt | 44 +- .../kstarbound/client/UserInput.kt | 4 + .../kstarbound/client/gl/GLAttributeList.kt | 7 +- .../kstarbound/client/gl/GLStateTracker.kt | 65 ++- .../client/gl/GLVertexBufferObject.kt | 15 + .../kstarbound/client/gl/VertexBuilder.kt | 430 +++++++++++++--- .../client/render/BakedProgramState.kt | 4 +- .../kstarbound/client/render/Camera.kt | 20 +- .../kstarbound/client/render/ChunkRenderer.kt | 17 +- .../kstarbound/client/render/Font.kt | 2 +- .../kstarbound/client/render/TileRenderer.kt | 41 +- .../ru/dbotthepony/kstarbound/math/AABB.kt | 476 ++++++++++++++++++ .../ru/dbotthepony/kstarbound/math/Matrix.kt | 4 + .../ru/dbotthepony/kstarbound/math/Utils.kt | 27 + .../ru/dbotthepony/kstarbound/math/Vector.kt | 328 +++++++++++- .../dbotthepony/kstarbound/util/Formatter.kt | 4 +- .../ru/dbotthepony/kstarbound/world/Chunk.kt | 93 +++- .../ru/dbotthepony/kstarbound/world/World.kt | 55 +- .../kstarbound/world/entities/AliveEntity.kt | 8 + .../kstarbound/world/entities/Entity.kt | 179 +++++++ .../kstarbound/world/entities/Humanoid.kt | 10 + .../shaders/fragment/flat_color.glsl | 9 + .../shaders/vertex/flat_vertex_2d.glsl | 9 + .../dbotthepony/kstarbound/test/MathTests.kt | 51 ++ 28 files changed, 1852 insertions(+), 191 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/UserInput.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Humanoid.kt create mode 100644 src/main/resources/shaders/fragment/flat_color.glsl create mode 100644 src/main/resources/shaders/vertex/flat_vertex_2d.glsl create mode 100644 src/test/kotlin/ru/dbotthepony/kstarbound/test/MathTests.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index a7979d00..076e0d3f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -2,22 +2,13 @@ package ru.dbotthepony.kstarbound import org.apache.logging.log4j.LogManager import org.lwjgl.Version -import org.lwjgl.glfw.GLFW.* -import org.lwjgl.opengl.GL46.* import ru.dbotthepony.kstarbound.client.StarboundClient -import ru.dbotthepony.kstarbound.math.Matrix4f -import ru.dbotthepony.kstarbound.client.render.Camera -import ru.dbotthepony.kstarbound.client.render.ChunkRenderer -import ru.dbotthepony.kstarbound.client.render.TextAlignX -import ru.dbotthepony.kstarbound.client.render.TextAlignY import ru.dbotthepony.kstarbound.defs.TileDefinition -import ru.dbotthepony.kstarbound.util.Color -import ru.dbotthepony.kstarbound.util.formatBytesShort -import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF +import ru.dbotthepony.kstarbound.math.Vector2d import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kstarbound.world.entities.Humanoid import java.io.File -import java.util.* private val LOGGER = LogManager.getLogger() @@ -41,43 +32,38 @@ fun main() { Starbound.onInitialize { chunkA = client.world!!.computeIfAbsent(ChunkPos(0, 0)).chunk val chunkB = client.world!!.computeIfAbsent(ChunkPos(-1, 0)).chunk - - var x = 0 - var y = 0 - - for (tile in Starbound.tilesAccess.values) { - //chunkA!!.background[x, y + 1] = tile - //chunkA!!.background[x++, y] = tile - - if (x >= 31) { - x = 0 - y += 2 - } - } - - x = 0 - y = 0 - - for (tile in Starbound.tilesAccess.values) { - //chunkB.foreground[x, y + 1] = tile - //chunkB.foreground[x++, y] = tile - - if (x > 31) { - x = 0 - y += 2 - } - } + val chunkC = client.world!!.computeIfAbsent(ChunkPos(-2, 0)).chunk val tile = Starbound.getTileDefinition("alienrock") - for (x in 0 .. 31) { - for (y in 0 .. 31) { - chunkA!!.foreground[x, y] = tile + for (x in -48 .. 48) { + for (y in 0 .. 20) { + val chnk = client.world!!.computeIfAbsent(ChunkPos(x, y)) + + for (bx in 0 .. 31) { + for (by in 0 .. 3) { + chnk.chunk.foreground[bx, by] = tile + } + } } } for (x in 0 .. 31) { - for (y in 0 .. 31) { + for (y in 0 .. 3) { + chunkA!!.foreground[x, y] = tile + chunkC.foreground[x, y] = tile + } + } + + for (x in 0 .. 31) { + for (y in 8 .. 9) { + chunkA!!.foreground[x, y] = tile + chunkC.foreground[x, y] = tile + } + } + + for (x in 0 .. 31) { + for (y in 0 .. 0) { chunkB.foreground[x, y] = tile } } @@ -87,16 +73,44 @@ fun main() { chunkA!!.foreground[x, y] = null as TileDefinition? } } + + /*val rand = Random() + + for (i in 0 .. 400) { + chunkA!!.foreground[rand.nextInt(0, CHUNK_SIZE_FF), rand.nextInt(0, CHUNK_SIZE_FF)] = tile + }*/ } - val rand = Random() + //val rand = Random() + val ent = Humanoid(client.world!!) + + ent.pos += Vector2d(y = 36.0, x = 10.0) + + client.onDrawGUI { + client.gl.font.render("${ent.pos}", y = 100f, scale = 0.25f) + } + + client.onPostDrawWorld { + client.gl.quadWireframe { + it.quad(ent.aabb + ent.pos) + } + } while (client.renderFrame()) { Starbound.pollCallbacks() - if (chunkA != null && glfwGetTime() < 10.0) { - val tile = Starbound.getTileDefinition("alienrock") + ent.moveAndCollide(client.frameRenderTime) + client.camera.pos.x = ent.pos.x.toFloat() + client.camera.pos.y = ent.pos.y.toFloat() + + //println(client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1) + + //if (ent.onGround) + ent.velocity += client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1 + + //if (chunkA != null && glfwGetTime() < 10.0) { + // val tile = Starbound.getTileDefinition("alienrock") //chunkA!!.foreground[rand.nextInt(0, CHUNK_SIZE_FF), rand.nextInt(0, CHUNK_SIZE_FF)] = tile - } + //} } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 9723c35b..12d297b4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -9,8 +9,8 @@ import ru.dbotthepony.kstarbound.world.World import java.io.File import java.io.FileNotFoundException -const val METRES_IN_STARBOUND_UNIT = 0.5 -const val METRES_IN_STARBOUND_UNITf = 0.5f +const val METRES_IN_STARBOUND_UNIT = 0.25 +const val METRES_IN_STARBOUND_UNITf = 0.25f const val PIXELS_IN_STARBOUND_UNIT = 8.0 const val PIXELS_IN_STARBOUND_UNITf = 8.0f diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientSettings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientSettings.kt index b34ca519..c19323a6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientSettings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientSettings.kt @@ -6,7 +6,7 @@ data class ClientSettings( * * Масштаб в единицу означает что один Starbound Unit будет равен 8 пикселям на экране */ - var scale: Float = 2f -) { + var scale: Float = 2f, -} + var debugCollisions: Boolean = true, +) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt index 52f09aab..b2d508bb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt @@ -1,12 +1,12 @@ package ru.dbotthepony.kstarbound.client +import ru.dbotthepony.kstarbound.api.IStruct2d import ru.dbotthepony.kstarbound.api.IStruct2f import ru.dbotthepony.kstarbound.client.render.ChunkRenderer import ru.dbotthepony.kstarbound.client.render.renderLayeredList -import ru.dbotthepony.kstarbound.world.Chunk -import ru.dbotthepony.kstarbound.world.IWorldChunkTuple -import ru.dbotthepony.kstarbound.world.MutableWorldChunkTuple -import ru.dbotthepony.kstarbound.world.World +import ru.dbotthepony.kstarbound.math.AABB +import ru.dbotthepony.kstarbound.math.Vector2d +import ru.dbotthepony.kstarbound.world.* class ClientWorldChunkTuple( world: World<*>, @@ -47,7 +47,7 @@ class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World() - for (chunk in chunkMap.values) { + for (chunk in collectInternal(size.encasingChunkPosAABB())) { determineRenderers.add(chunk.renderer) } - val renderList = ArrayList() - for (renderer in determineRenderers) { - renderList.add(renderer) renderer.autoBakeStatic() } - renderLayeredList(client.gl.matrixStack, renderList) + renderLayeredList(client.gl.matrixStack, determineRenderers) + + for (renderer in determineRenderers) { + renderer.renderDebug() + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index a44b645f..148a7c34 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -7,11 +7,15 @@ import org.lwjgl.glfw.GLFWErrorCallback import org.lwjgl.opengl.GL46.* import org.lwjgl.system.MemoryStack import org.lwjgl.system.MemoryUtil +import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNIT +import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.client.gl.GLStateTracker import ru.dbotthepony.kstarbound.math.Matrix4f import ru.dbotthepony.kstarbound.client.render.Camera import ru.dbotthepony.kstarbound.client.render.TextAlignX import ru.dbotthepony.kstarbound.client.render.TextAlignY +import ru.dbotthepony.kstarbound.math.AABB +import ru.dbotthepony.kstarbound.math.Vector2d import ru.dbotthepony.kstarbound.math.Vector2f import ru.dbotthepony.kstarbound.util.Color import ru.dbotthepony.kstarbound.util.formatBytesShort @@ -159,6 +163,24 @@ class StarboundClient : AutoCloseable { val settings = ClientSettings() + private val onDrawGUI = ArrayList<() -> Unit>() + + fun onDrawGUI(lambda: () -> Unit) { + onDrawGUI.add(lambda) + } + + private val onPreDrawWorld = ArrayList<() -> Unit>() + + fun onPreDrawWorld(lambda: () -> Unit) { + onPreDrawWorld.add(lambda) + } + + private val onPostDrawWorld = ArrayList<() -> Unit>() + + fun onPostDrawWorld(lambda: () -> Unit) { + onPostDrawWorld.add(lambda) + } + fun renderFrame(): Boolean { ensureSameThread() @@ -176,10 +198,22 @@ class StarboundClient : AutoCloseable { val maxs = -mins gl.matrixStack.push() - .translateWithScale(viewportWidth / 2f - camera.pos.x, viewportHeight / 2f - camera.pos.y) // центр экрана + координаты отрисовки мира - .scale(x = settings.scale, y = settings.scale) // масштабируем до нужного размера + .translateWithScale(viewportWidth / 2f, viewportHeight / 2f, 2f) // центр экрана + координаты отрисовки мира + .scale(x = settings.scale * PIXELS_IN_STARBOUND_UNITf, y = settings.scale * PIXELS_IN_STARBOUND_UNITf) // масштабируем до нужного размера + .translateWithScale(-camera.pos.x, -camera.pos.y) // перемещаем вид к камере - world?.render(Vector2f.ZERO, mins = mins, maxs = maxs) + for (lambda in onPreDrawWorld) { + lambda.invoke() + } + + world?.render(AABB.rectangle( + camera.pos.toDoubleVector(), + viewportWidth / settings.scale / PIXELS_IN_STARBOUND_UNIT, + viewportHeight / settings.scale / PIXELS_IN_STARBOUND_UNIT)) + + for (lambda in onPostDrawWorld) { + lambda.invoke() + } gl.matrixStack.pop() @@ -210,6 +244,10 @@ class StarboundClient : AutoCloseable { gl.matrixStack.pop() } + for (fn in onDrawGUI) { + fn.invoke() + } + val runtime = Runtime.getRuntime() gl.font.render("FPS: ${(averageFramesPerSecond * 100f).toInt() / 100f}", scale = 0.4f) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/UserInput.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/UserInput.kt new file mode 100644 index 00000000..18a98c9c --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/UserInput.kt @@ -0,0 +1,4 @@ +package ru.dbotthepony.kstarbound.client + +class UserInput { +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLAttributeList.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLAttributeList.kt index 0ba7c84c..974c246a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLAttributeList.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLAttributeList.kt @@ -38,11 +38,15 @@ data class AttributeListPosition(val name: String, val index: Int, val glType: G class GLFlatAttributeList(builder: GLFlatAttributeListBuilder) : IGLAttributeList { val attributes: List val size get() = attributes.size + + /** + * Шаг данных аттрибутов, в байтах. Т.е. одна полная вершина будет занимать [stride] байт в памяти. + */ val stride: Int operator fun get(index: Int) = attributes[index] - fun vertexBuilder(vertexType: VertexType) = VertexBuilder(this, vertexType) + fun vertexBuilder(vertexType: VertexType) = DynamicVertexBuilder(this, vertexType) init { val buildList = ArrayList() @@ -81,6 +85,7 @@ class GLFlatAttributeList(builder: GLFlatAttributeListBuilder) : IGLAttributeLis } companion object { + val VEC2F = GLFlatAttributeListBuilder().also {it.push(GLType.VEC2F)}.build() val VEC3F = GLFlatAttributeListBuilder().also {it.push(GLType.VEC3F)}.build() val VERTEX_TEXTURE = GLFlatAttributeListBuilder().also {it.push(GLType.VEC3F).push(GLType.VEC2F)}.build() val VERTEX_2D_TEXTURE = GLFlatAttributeListBuilder().also {it.push(GLType.VEC2F).push(GLType.VEC2F)}.build() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt index e3d7b584..9414dd0b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt @@ -9,7 +9,6 @@ import ru.dbotthepony.kstarbound.client.freetype.FreeType import ru.dbotthepony.kstarbound.math.Matrix4f import ru.dbotthepony.kstarbound.math.Matrix4fStack import ru.dbotthepony.kstarbound.client.render.Font -import ru.dbotthepony.kstarbound.client.render.TileRenderer import ru.dbotthepony.kstarbound.client.render.TileRenderers import ru.dbotthepony.kstarbound.util.Color import java.io.File @@ -83,6 +82,11 @@ interface GLCleanable : Cleaner.Cleanable { fun cleanManual(): Unit } +interface GLStreamBuilderList { + val small: StreamVertexBuilder + val statefulSmall: StatefulStreamVertexBuilder +} + class GLStateTracker { init { // This line is critical for LWJGL's interoperation with GLFW's @@ -329,11 +333,70 @@ class GLStateTracker { fragment.unlink() } + val flatProgram: GLTransformableColorableProgram + + init { + val vertex = GLShader.internalVertex("shaders/vertex/flat_vertex_2d.glsl") + val fragment = GLShader.internalFragment("shaders/fragment/flat_color.glsl") + + flatProgram = GLTransformableColorableProgram(this, vertex, fragment) + + vertex.unlink() + fragment.unlink() + } + + val flat2DQuads = object : GLStreamBuilderList { + override val small by lazy { + return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.QUADS, 1024) + } + + override val statefulSmall by lazy { + return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small) + } + } + + val flat2DQuadLines = object : GLStreamBuilderList { + override val small by lazy { + return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.QUADS_AS_LINES, 1024) + } + + override val statefulSmall by lazy { + return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small) + } + } + + val flat2DQuadWireframe = object : GLStreamBuilderList { + override val small by lazy { + return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.QUADS_AS_LINES_WIREFRAME, 1024) + } + + override val statefulSmall by lazy { + return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small) + } + } + val matrixStack = Matrix4fStack() val freeType = FreeType() val font = Font(this) + fun quadWireframe(lambda: (StreamVertexBuilder) -> Unit) { + val stateful = flat2DQuadWireframe.statefulSmall + val builder = stateful.builder + + builder.begin() + + lambda.invoke(builder) + + stateful.upload() + + flatProgram.use() + flatProgram.color.set(Color.WHITE) + flatProgram.transform.set(matrixStack.last) + + stateful.draw(GL_LINES) + } + companion object { private val LOGGER = LogManager.getLogger(GLStateTracker::class.java) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLVertexBufferObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLVertexBufferObject.kt index 4043ddb5..ac62cc1d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLVertexBufferObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLVertexBufferObject.kt @@ -1,6 +1,7 @@ package ru.dbotthepony.kstarbound.client.gl import org.lwjgl.opengl.GL46.* +import org.lwjgl.system.MemoryUtil import java.io.Closeable import java.nio.ByteBuffer @@ -41,6 +42,20 @@ class GLVertexBufferObject(val state: GLStateTracker, val type: VBOType = VBOTyp return this } + fun bufferData(data: ByteBuffer, usage: Int, length: Long): GLVertexBufferObject { + check(isValid) { "Tried to use NULL GLVertexBufferObject" } + state.ensureSameThread() + + if (length > data.remaining().toLong()) { + throw IndexOutOfBoundsException("Tried to upload $data into $pointer with offset at ${data.position()} with length of $length, but that is longer than remaining data length of ${data.remaining()}!") + } + + nglNamedBufferData(pointer, length, MemoryUtil.memAddress(data), usage) + + checkForGLError() + return this + } + fun bufferData(data: IntArray, usage: Int): GLVertexBufferObject { check(isValid) { "Tried to use NULL GLVertexBufferObject" } state.ensureSameThread() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt index 50df99b1..246d6f1d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt @@ -1,16 +1,87 @@ package ru.dbotthepony.kstarbound.client.gl import org.lwjgl.opengl.GL46.* +import ru.dbotthepony.kstarbound.math.AABB +import java.io.Closeable import java.nio.ByteBuffer import java.nio.ByteOrder +import kotlin.collections.ArrayList enum class VertexType(val elements: Int, val indicies: IntArray) { + LINES(2, intArrayOf(0, 1)), TRIANGLES(3, intArrayOf(0, 1, 2)), - QUADS(4, intArrayOf(0, 1, 2, 1, 2, 3)) + QUADS(4, intArrayOf(0, 1, 2, 1, 2, 3)), + QUADS_AS_LINES(4, intArrayOf(0, 1, 0, 2, 1, 3, 2, 3)), + QUADS_AS_LINES_WIREFRAME(4, intArrayOf(0, 1, 0, 2, 1, 3, 2, 3, 0, 3, 1, 2)), } -typealias VertexTransformer = (VertexBuilder.Vertex, Int) -> VertexBuilder.Vertex +interface IVertexBuilder, VertexType : IVertex> { + val type: ru.dbotthepony.kstarbound.client.gl.VertexType + val indexCount: Int + + fun begin(): This + fun vertex(): VertexType + fun checkValid() + fun upload(vbo: GLVertexBufferObject, ebo: GLVertexBufferObject, drawType: Int = GL_DYNAMIC_DRAW) + + fun quad( + x0: Float, + y0: Float, + x1: Float, + y1: Float, + lambda: VertexTransformer = emptyTransform + ): This { + check(type.elements == 4) { "Currently building $type" } + + lambda(vertex().pushVec2f(x0, y0), 0).end() + lambda(vertex().pushVec2f(x1, y0), 1).end() + lambda(vertex().pushVec2f(x0, y1), 2).end() + lambda(vertex().pushVec2f(x1, y1), 3).end() + + return this as This + } + + fun quad(aabb: AABB, lambda: VertexTransformer = emptyTransform): This { + return quad( + aabb.mins.x.toFloat(), + aabb.mins.y.toFloat(), + aabb.maxs.x.toFloat(), + aabb.maxs.y.toFloat(), + lambda + ) + } + + fun quadZ( + x0: Float, + y0: Float, + x1: Float, + y1: Float, + z: Float, + lambda: VertexTransformer = emptyTransform + ): This { + check(type.elements == 4) { "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 as This + } +} + +interface IVertex, VertexBuilderType> { + fun checkValid() + fun expect(name: String): This + fun expect(type: GLType): This + fun pushVec3f(x: Float, y: Float, z: Float): This + fun pushVec2f(x: Float, y: Float): This + fun end(): VertexBuilderType +} + +typealias VertexTransformer = (IVertex<*, *>, Int) -> IVertex<*, *> private val emptyTransform: VertexTransformer = { it, _ -> it } +private val EMPTY_BUFFER = ByteBuffer.allocateDirect(0) object VertexTransformers { fun uv(u0: Float, @@ -58,111 +129,93 @@ object VertexTransformers { } } -class VertexBuilder(val attributes: GLFlatAttributeList, private val type: VertexType) { +class DynamicVertexBuilder(val attributes: GLFlatAttributeList, override val type: VertexType) : IVertexBuilder { private val verticies = ArrayList() - val indexCount get() = (verticies.size / type.elements) * type.indicies.size + override val indexCount get() = (verticies.size / type.elements) * type.indicies.size - fun begin(): VertexBuilder { + override fun begin(): DynamicVertexBuilder { verticies.clear() return this } - fun vertex(): Vertex { + override 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 quad( - x0: Float, - y0: Float, - x1: Float, - y1: Float, - lambda: VertexTransformer = emptyTransform - ): VertexBuilder { - check(type == VertexType.QUADS) { "Currently building $type" } - - lambda(Vertex().pushVec2f(x0, y0), 0).end() - lambda(Vertex().pushVec2f(x1, y0), 1).end() - lambda(Vertex().pushVec2f(x0, y1), 2).end() - lambda(Vertex().pushVec2f(x1, y1), 3).end() - - return this - } - - fun checkValid() { + override fun checkValid() { for (vertex in verticies) { vertex.checkValid() } } + /** + * Загружает (копирует) данные в указанные буферы, с их текущей позиции + */ + fun upload( + vertexBuffer: ByteBuffer, + elementBuffer: ByteBuffer, + ) { + 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})" } + + require(vertexBuffer.order() == ByteOrder.nativeOrder()) { "Byte order of $vertexBuffer does not match native order" } + require(elementBuffer.order() == ByteOrder.nativeOrder()) { "Byte order of $elementBuffer does not match native order" } + + checkValid() + + for (vertex in verticies) { + vertex.upload(vertexBuffer) + } + + var offsetVertex = 0 + + for (i in 0 until verticies.size / type.elements) { + for (i2 in type.indicies.indices) { + elementBuffer.putInt(type.indicies[i2] + offsetVertex) + } + + offsetVertex += type.elements + } + } + /** * Загружает буфер в указанные VBO и EBO * * операция создаёт мусор вне кучи и довольно медленная */ - fun upload(vbo: GLVertexBufferObject, ebo: GLVertexBufferObject, drawType: Int = GL_DYNAMIC_DRAW) { + override fun upload(vbo: GLVertexBufferObject, ebo: GLVertexBufferObject, drawType: Int) { 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() + checkValid() if (verticies.size == 0) { - vbo.bufferData(intArrayOf(), drawType) - ebo.bufferData(intArrayOf(), drawType) + vbo.bufferData(EMPTY_BUFFER, drawType) + ebo.bufferData(EMPTY_BUFFER, drawType) + return } - val bytes = ByteBuffer.allocateDirect(verticies.size * attributes.stride) - bytes.order(ByteOrder.nativeOrder()) + val vertexBuffer = ByteBuffer.allocateDirect(verticies.size * attributes.stride) + vertexBuffer.order(ByteOrder.nativeOrder()) - for (vertex in verticies) { - vertex.upload(bytes) - } + val elementBuffer = ByteBuffer.allocateDirect((verticies.size / type.elements) * type.indicies.size * 4) + elementBuffer.order(ByteOrder.nativeOrder()) - check(bytes.position() == bytes.capacity()) { "Buffer is not fully filled (position: ${bytes.position()}; capacity: ${bytes.capacity()})" } + upload(vertexBuffer, elementBuffer) - bytes.position(0) - vbo.bufferData(bytes, drawType) + check(vertexBuffer.position() == vertexBuffer.capacity()) { "Vertex Buffer is not fully filled (position: ${vertexBuffer.position()}; capacity: ${vertexBuffer.capacity()})" } + check(elementBuffer.position() == elementBuffer.capacity()) { "Element Buffer is not fully filled (position: ${elementBuffer.position()}; capacity: ${elementBuffer.capacity()})" } - val elementIndicies = IntArray((verticies.size / type.elements) * type.indicies.size) - var offset = 0 - var offsetVertex = 0 + vertexBuffer.position(0) + elementBuffer.position(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) + vbo.bufferData(vertexBuffer, drawType) + ebo.bufferData(elementBuffer, drawType) } - inner class Vertex { + inner class Vertex : IVertex { init { verticies.add(this) } @@ -193,7 +246,7 @@ class VertexBuilder(val attributes: GLFlatAttributeList, private val type: Verte } }.joinToString("; ")})" } - fun expect(name: String): Vertex { + override fun expect(name: String): Vertex { if (index >= attributes.size) { throw IllegalStateException("Reached end of attribute list early, expected $name") } @@ -205,7 +258,7 @@ class VertexBuilder(val attributes: GLFlatAttributeList, private val type: Verte return this } - fun expect(type: GLType): Vertex { + override fun expect(type: GLType): Vertex { if (index >= attributes.size) { throw IllegalStateException("Reached end of attribute list early, expected type $type") } @@ -217,19 +270,19 @@ class VertexBuilder(val attributes: GLFlatAttributeList, private val type: Verte return this } - fun pushVec3f(x: Float, y: Float, z: Float): Vertex { + override 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 { + override fun pushVec2f(x: Float, y: Float): Vertex { expect(GLType.VEC2F) store[index++] = floatArrayOf(x, y) return this } - fun checkValid() { + override fun checkValid() { for (elem in store.indices) { if (store[elem] == null) { throw IllegalStateException("Vertex element at position $elem is null") @@ -237,10 +290,231 @@ class VertexBuilder(val attributes: GLFlatAttributeList, private val type: Verte } } - fun end(): VertexBuilder { + override fun end(): DynamicVertexBuilder { checkValid() - return this@VertexBuilder + return this@DynamicVertexBuilder } } } +/** + * "Поточная" версия [DynamicVertexBuilder], ориентированная на скорость работы и имеющая фиксированный размер буфера + * + * Главные отличия: + * * Данный объект не желательно создавать каждый раз когда надо отрисовать какое либо количество геометрии, а использовать уже существующий, который + * удовлетворяет требованиям (формат вершин, и их потенциально максимальное количество) + * * Максимальное количество vertex'ов фиксированно и равняется [maxElements] * [VertexType.elements] + * * Имеет два встроенных DirectByteBuffer и НЕ позволяет загружать данные в другие ByteBuffer, только во внутренние ByteBuffer и только в VBO; EBO + */ +class StreamVertexBuilder( + val attributes: GLFlatAttributeList, + override val type: VertexType, + val maxElements: Int, +) : IVertexBuilder { + val maxVertexNum = maxElements * type.elements + var nextVertex = 0 + private set + + override val indexCount get() = (nextVertex / type.elements) * type.indicies.size + val maxIndexCount = maxElements * type.indicies.size + + val elementIndexType = when (maxIndexCount) { + // api performance issue 102: glDrawElements uses element index type 'GL_UNSIGNED_BYTE' that is not optimal for the current hardware configuration; consider using 'GL_UNSIGNED_SHORT' instead + // in 0 .. 255 -> GL_UNSIGNED_BYTE + in 0 .. 65535 -> GL_UNSIGNED_SHORT + else -> GL_UNSIGNED_INT + } + + private var head: Vertex? = null + + /** + * Буфер для VBO достаточного размера + */ + private val vertexBuffer = ByteBuffer.allocateDirect(maxVertexNum * attributes.stride) + + /** + * Буфер для EBO достаточного размера + */ + private val elementBuffer = ByteBuffer.allocateDirect(maxElements * type.indicies.size * 4) + + init { + vertexBuffer.order(ByteOrder.nativeOrder()) + elementBuffer.order(ByteOrder.nativeOrder()) + } + + private fun writeElementIndex(value: Int) { + when (elementIndexType) { + GL_UNSIGNED_BYTE -> elementBuffer.put(value.toByte()) + GL_UNSIGNED_SHORT -> elementBuffer.putShort(value.toShort()) + else -> elementBuffer.putInt(value) + } + } + + private var offsetElementIndex = 0 + + /** + * Устанавливает метку этого билдера в ноль. + * + * Не обнуляет память буферов! + */ + override fun begin(): StreamVertexBuilder { + nextVertex = 0 + offsetElementIndex = 0 + head = null + vertexBuffer.position(0) + elementBuffer.position(0) + return this + } + + override fun vertex(): Vertex { + return Vertex() + } + + override fun upload(vbo: GLVertexBufferObject, ebo: GLVertexBufferObject, drawType: Int) { + require(vbo.isArray) { "$vbo is not an array" } + require(ebo.isElementArray) { "$vbo is not an element array" } + + if (nextVertex == 0) { + vbo.bufferData(EMPTY_BUFFER, drawType) + ebo.bufferData(EMPTY_BUFFER, drawType) + + return + } + + checkValid() + + val a = vertexBuffer.position().toLong() + val b = elementBuffer.position().toLong() + + vertexBuffer.position(0) + elementBuffer.position(0) + + vbo.bufferData(vertexBuffer, drawType, length = a) + ebo.bufferData(elementBuffer, drawType, length = b) + } + + override fun checkValid() { + var vertex = head + + while (vertex != null) { + vertex.checkValid() + vertex = vertex.previous + } + } + + inner class Vertex : IVertex { + private val vertexIndex = nextVertex++ + val previous = head + private var bufferPosition = vertexIndex * attributes.stride + + init { + if (vertexIndex >= maxVertexNum) { + throw IndexOutOfBoundsException("Tried to push new vertex $vertexIndex, when already above limit of $maxVertexNum!") + } + + head = this + + for (i2 in type.indicies.indices) { + writeElementIndex(type.indicies[i2] + offsetElementIndex) + } + + offsetElementIndex += type.elements + } + + private var index = 0 + + override fun checkValid() { + check(index == attributes.size) { "Vertex $vertexIndex is not fully filled (only $index attributes provided, ${attributes.size} required)" } + } + + override 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 + } + + override 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 + } + + override fun pushVec3f(x: Float, y: Float, z: Float): Vertex { + expect(GLType.VEC3F) + vertexBuffer.position(bufferPosition) + vertexBuffer.putFloat(x) + vertexBuffer.putFloat(y) + vertexBuffer.putFloat(z) + index++ + bufferPosition += 12 + return this + } + + override fun pushVec2f(x: Float, y: Float): Vertex { + expect(GLType.VEC2F) + vertexBuffer.position(bufferPosition) + vertexBuffer.putFloat(x) + vertexBuffer.putFloat(y) + index++ + bufferPosition += 8 + return this + } + + override fun end(): StreamVertexBuilder { + check(index == attributes.size) { "Vertex $vertexIndex is not fully filled (only $index attributes provided, ${attributes.size} required)" } + return this@StreamVertexBuilder + } + } +} + +class StatefulStreamVertexBuilder( + val state: GLStateTracker, + val builder: StreamVertexBuilder +) : Closeable, IVertexBuilder by builder { + private val vao = state.newVAO() + private val vbo = state.newVBO() + private val ebo = state.newEBO() + + init { + vao.bind() + vbo.bind() + ebo.bind() + + builder.attributes.apply(vao, true) + + vao.unbind() + vbo.unbind() + ebo.unbind() + } + + fun upload(drawType: Int = GL_DYNAMIC_DRAW) { + builder.upload(vbo, ebo, drawType) + } + + fun bind() = vao.bind() + fun unbind() = vao.unbind() + + fun draw(primitives: Int = GL_TRIANGLES) { + bind() + glDrawElements(primitives, builder.indexCount, builder.elementIndexType, 0L) + checkForGLError() + } + + override fun close() { + vao.close() + vbo.close() + ebo.close() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BakedProgramState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BakedProgramState.kt index cec83cac..ba343b9d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BakedProgramState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BakedProgramState.kt @@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound.client.render import org.lwjgl.opengl.GL46.* import ru.dbotthepony.kstarbound.client.gl.GLShaderProgram import ru.dbotthepony.kstarbound.client.gl.GLVertexArrayObject -import ru.dbotthepony.kstarbound.client.gl.VertexBuilder +import ru.dbotthepony.kstarbound.client.gl.DynamicVertexBuilder import ru.dbotthepony.kstarbound.client.gl.checkForGLError import ru.dbotthepony.kstarbound.math.FloatMatrix @@ -45,7 +45,7 @@ class BakedStaticMesh( ) : AutoCloseable { private var onClose = {} - constructor(programState: BakedProgramState, builder: VertexBuilder) : this( + constructor(programState: BakedProgramState, builder: DynamicVertexBuilder) : this( programState, builder.indexCount, programState.program.state.newVAO(), diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Camera.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Camera.kt index 0883585f..be4892e0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Camera.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Camera.kt @@ -7,7 +7,7 @@ class Camera { /** * Позиция этой камеры в Starbound Unit'ах */ - val pos = MutableVector3f() + val pos = MutableVector2f() var pressedLeft = false private set @@ -30,22 +30,30 @@ class Camera { } } - fun tick(delta: Double) { + val velocity: MutableVector2f get() { + val vec = MutableVector2f() + if (pressedLeft) { - pos.x -= (delta * FREEVIEW_SENS).toFloat() + vec.x -= (FREEVIEW_SENS).toFloat() } if (pressedRight) { - pos.x += (delta * FREEVIEW_SENS).toFloat() + vec.x += (FREEVIEW_SENS).toFloat() } if (pressedUp) { - pos.y += (delta * FREEVIEW_SENS).toFloat() + vec.y += (FREEVIEW_SENS).toFloat() } if (pressedDown) { - pos.y -= (delta * FREEVIEW_SENS).toFloat() + vec.y -= (FREEVIEW_SENS).toFloat() } + + return vec + } + + fun tick(delta: Double) { + pos + velocity * delta.toFloat() } companion object { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt index 1eee4d55..a4c8b6c2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt @@ -7,6 +7,7 @@ import ru.dbotthepony.kstarbound.math.FloatMatrix import ru.dbotthepony.kstarbound.math.Matrix4f import ru.dbotthepony.kstarbound.math.Matrix4fStack import ru.dbotthepony.kstarbound.world.CHUNK_SIZE +import ru.dbotthepony.kstarbound.world.CHUNK_SIZEf import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.ITileChunk import kotlin.collections.ArrayList @@ -117,6 +118,8 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Clie } } + val debugCollisions get() = world?.client?.settings?.debugCollisions ?: false + val transform = Matrix4f().translate(x = chunk.pos.x * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf, y = chunk.pos.x * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf) private val unloadableBakedMeshes = ArrayList() @@ -217,13 +220,25 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Clie foreground.autoUpload() } + fun renderDebug() { + if (debugCollisions) { + state.quadWireframe { + it.quad(chunk.aabb.mins.x.toFloat(), chunk.aabb.mins.y.toFloat(), chunk.aabb.maxs.x.toFloat(), chunk.aabb.maxs.y.toFloat()) + + for (layer in chunk.foreground.collisionLayers()) { + it.quad(layer.mins.x.toFloat(), layer.mins.y.toFloat(), layer.maxs.x.toFloat(), layer.maxs.y.toFloat()) + } + } + } + } + private val meshDeque = ArrayDeque>() override fun renderLayerFromStack(zPos: Int, transform: Matrix4fStack): Int { if (meshDeque.isEmpty()) return -1 - transform.push().translateWithScale(x = chunk.pos.x * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf, y = chunk.pos.y * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf) + transform.push().translateWithScale(x = chunk.pos.x * CHUNK_SIZEf, y = chunk.pos.y * CHUNK_SIZEf) var pair = meshDeque.last() while (pair.second >= zPos) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt index 43fd64f3..ca6308a9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt @@ -301,7 +301,7 @@ class Font( ebo.bind() vbo.bind() - val builder = VertexBuilder(GLFlatAttributeList.VERTEX_2D_TEXTURE, VertexType.QUADS) + val builder = DynamicVertexBuilder(GLFlatAttributeList.VERTEX_2D_TEXTURE, VertexType.QUADS) builder.quad(0f, 0f, width, height, VertexTransformers.uv()) builder.upload(vbo, ebo, GL_STATIC_DRAW) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt index 1fe45b4c..b680d55a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt @@ -15,13 +15,13 @@ import kotlin.collections.HashMap data class TileLayer( val bakedProgramState: BakedProgramState, - val vertexBuilder: VertexBuilder, + val vertexBuilder: DynamicVertexBuilder, val zPos: Int) class TileLayerList { private val layers = HashMap>() - fun getLayer(programState: BakedProgramState, zLevel: Int, compute: () -> VertexBuilder): VertexBuilder { + fun getLayer(programState: BakedProgramState, zLevel: Int, compute: () -> DynamicVertexBuilder): DynamicVertexBuilder { val list = layers.computeIfAbsent(programState) {ArrayList()} for (layer in list) { @@ -160,25 +160,25 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) { val bakedBackgroundProgramState = state.tileRenderers.background(texture) // private var notifiedDepth = false - private fun tesselateAt(piece: TileRenderPiece, getter: ITileChunk, builder: VertexBuilder, pos: Vector2i, offset: Vector2i = Vector2i.ZERO) { + private fun tesselateAt(piece: TileRenderPiece, getter: ITileChunk, builder: DynamicVertexBuilder, 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 + var c = fx + piece.textureSize.x / PIXELS_IN_STARBOUND_UNITf + var d = fy + piece.textureSize.y / PIXELS_IN_STARBOUND_UNITf if (offset != Vector2i.ZERO) { - a += offset.x / BASELINE_TEXTURE_SIZE + a += offset.x / PIXELS_IN_STARBOUND_UNITf // в json файлах y указан как положительный вверх, // что соответствует нашему миру - b += offset.y / BASELINE_TEXTURE_SIZE + b += offset.y / PIXELS_IN_STARBOUND_UNITf - c += offset.x / BASELINE_TEXTURE_SIZE - d += offset.y / BASELINE_TEXTURE_SIZE + c += offset.x / PIXELS_IN_STARBOUND_UNITf + d += offset.y / PIXELS_IN_STARBOUND_UNITf } if (tile.render.variants == 0 || piece.texture != null || piece.variantStride == null) { @@ -186,10 +186,10 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) { val (u1, v1) = texture.pixelToUV(piece.texturePosition + piece.textureSize) builder.quadZ( - a * PIXELS_IN_STARBOUND_UNITf, - b * PIXELS_IN_STARBOUND_UNITf, - c * PIXELS_IN_STARBOUND_UNITf, - d * PIXELS_IN_STARBOUND_UNITf, + a, + b, + c, + d, Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0)) } else { val variant = (getter.randomDoubleFor(pos) * tile.render.variants).toInt() @@ -198,15 +198,15 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) { val (u1, v1) = texture.pixelToUV(piece.texturePosition + piece.textureSize + piece.variantStride * variant) builder.quadZ( - a * PIXELS_IN_STARBOUND_UNITf, - b * PIXELS_IN_STARBOUND_UNITf, - c * PIXELS_IN_STARBOUND_UNITf, - d * PIXELS_IN_STARBOUND_UNITf, + a, + b, + c, + d, Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0)) } } - private fun tesselatePiece(matchPiece: TileRenderMatchPiece, getter: ITileChunk, layers: TileLayerList, pos: Vector2i, thisBuilder: VertexBuilder, background: Boolean): TileRenderTesselateResult { + private fun tesselatePiece(matchPiece: TileRenderMatchPiece, getter: ITileChunk, layers: TileLayerList, pos: Vector2i, thisBuilder: DynamicVertexBuilder, background: Boolean): TileRenderTesselateResult { if (matchPiece.test(getter, tile, pos)) { for (renderPiece in matchPiece.pieces) { if (renderPiece.piece.texture != null) { @@ -219,7 +219,7 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) { } tesselateAt(renderPiece.piece, getter, layers.getLayer(program, tile.render.zLevel) { - return@getLayer VertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS) + return@getLayer DynamicVertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS) }, pos, renderPiece.offset) } else { tesselateAt(renderPiece.piece, getter, thisBuilder, pos, renderPiece.offset) @@ -259,7 +259,7 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) { tile.render.renderTemplate ?: return val builder = layers.getLayer(if (background) bakedBackgroundProgramState else bakedProgramState, tile.render.zLevel) { - return@getLayer VertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS) + return@getLayer DynamicVertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS) } for ((_, matcher) in tile.render.renderTemplate.matches) { @@ -274,7 +274,6 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) { } companion object { - const val BASELINE_TEXTURE_SIZE = 8f const val Z_LEVEL = 10f private val LOGGER = LogManager.getLogger() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt new file mode 100644 index 00000000..3879648e --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt @@ -0,0 +1,476 @@ +package ru.dbotthepony.kstarbound.math + +import ru.dbotthepony.kstarbound.api.IStruct2d +import ru.dbotthepony.kstarbound.world.ChunkPos +import kotlin.math.absoluteValue + +data class IntersectionTime( + val invEntry: Vector2d, + val invExit: Vector2d, + val entry: Vector2d, + val exit: Vector2d, +) { + companion object { + val ZERO = IntersectionTime(Vector2d.ZERO, Vector2d.ZERO, Vector2d.ZERO, Vector2d.ZERO) + } +} + +data class SweepResult( + val normal: Vector2d, + val collisionTime: Double, + val intersectionTime: IntersectionTime +) { + companion object { + val ZERO = SweepResult(Vector2d.ZERO, 1.0, IntersectionTime.ZERO) + val INTERSECT = SweepResult(Vector2d.ZERO, 0.0, IntersectionTime.ZERO) + } +} + +/** + * Класс для описания Axis Aligned Bounding Box, двумя векторами, + * где [mins] - нижняя левая точка, + * [maxs] - верхняя правая + */ +data class AABB(val mins: Vector2d, val maxs: Vector2d) { + init { + require(mins.x < maxs.x) { "mins.x ${mins.x} is more or equal to maxs.x ${maxs.x}" } + require(mins.y < maxs.y) { "mins.y ${mins.y} is more or equal to maxs.y ${maxs.y}" } + } + + operator fun plus(other: AABB) = AABB(mins + other.mins, maxs + other.maxs) + operator fun minus(other: AABB) = AABB(mins - other.mins, maxs - other.maxs) + operator fun times(other: AABB) = AABB(mins * other.mins, maxs * other.maxs) + operator fun div(other: AABB) = AABB(mins / other.mins, maxs / other.maxs) + + operator fun plus(other: Vector2d) = AABB(mins + other, maxs + other) + operator fun minus(other: Vector2d) = AABB(mins - other, maxs - other) + operator fun times(other: Vector2d) = AABB(mins * other, maxs * other) + operator fun div(other: Vector2d) = AABB(mins / other, maxs / other) + + operator fun plus(other: Double) = AABB(mins + other, maxs + other) + operator fun minus(other: Double) = AABB(mins - other, maxs - other) + operator fun times(other: Double) = AABB(mins * other, maxs * other) + operator fun div(other: Double) = AABB(mins / other, maxs / other) + + val xSpan get() = maxs.x - mins.x + val ySpan get() = maxs.y - mins.y + val centre get() = mins + maxs * 0.5 + + val A get() = mins + val B get() = Vector2d(mins.x, maxs.y) + val C get() = maxs + val D get() = Vector2d(maxs.x, mins.y) + + val bottomLeft get() = A + val topLeft get() = B + val topRight get() = C + val bottomRight get() = D + + val width get() = (maxs.x - mins.x) / 2.0 + val height get() = (maxs.y - mins.y) / 2.0 + + val diameter get() = mins.distance(maxs) + val radius get() = diameter / 2.0 + + fun isInside(point: Vector2d): Boolean { + return point.x in mins.x .. maxs.x && point.y in mins.y .. maxs.y + } + + /** + * Есть ли пересечение между этим AABB и [other] + * + * Считается, что они пересекаются, даже если у них просто равна одна из осей + */ + fun intersect(other: AABB): Boolean { + val intersectX: Boolean + + if (xSpan <= other.xSpan) + intersectX = mins.x in other.mins.x .. other.maxs.x || maxs.x in other.mins.x .. other.maxs.x + else + intersectX = other.mins.x in mins.x .. maxs.x || other.maxs.x in mins.x .. maxs.x + + if (!intersectX) + return false + + val intersectY: Boolean + + if (ySpan <= other.ySpan) + intersectY = mins.y in other.mins.y .. other.maxs.y || maxs.y in other.mins.y .. other.maxs.y + else + intersectY = other.mins.y in mins.y .. maxs.y || other.maxs.y in mins.y .. maxs.y + + return intersectY + } + + /** + * Есть ли пересечение между этим AABB и [other] + * + * Считается, что они НЕ пересекаются, если у них просто равна одна из осей + */ + fun intersectWeak(other: AABB): Boolean { + if (maxs.x == other.mins.x || mins.x == other.maxs.x || maxs.y == other.mins.y || mins.y == other.maxs.y) + return false + + val intersectX: Boolean + + if (xSpan <= other.xSpan) + intersectX = mins.x in other.mins.x .. other.maxs.x || maxs.x in other.mins.x .. other.maxs.x + else + intersectX = other.mins.x in mins.x .. maxs.x || other.maxs.x in mins.x .. maxs.x + + if (!intersectX) + return false + + val intersectY: Boolean + + if (ySpan <= other.ySpan) + intersectY = mins.y in other.mins.y .. other.maxs.y || maxs.y in other.mins.y .. other.maxs.y + else + intersectY = other.mins.y in mins.y .. maxs.y || other.maxs.y in mins.y .. maxs.y + + return intersectY + } + + fun intersectionDepth(other: AABB): Vector2d { + val xDepth: Double + val yDepth: Double + + val thisCentre = centre + val otherCentre = other.centre + + if (thisCentre.x > otherCentre.x) { + // считаем, что мы вошли справа + xDepth = mins.x - other.maxs.x + } else { + // считаем, что мы вошли слева + xDepth = maxs.x - other.mins.x + } + + if (thisCentre.y > otherCentre.y) { + // считаем, что мы вошли сверху + yDepth = mins.y - other.maxs.y + } else { + // считаем, что мы вошли снизу + yDepth = maxs.x - other.mins.x + } + + return Vector2d(xDepth, yDepth) + } + + fun pushOutFrom(other: AABB): Vector2d { + if (!intersect(other)) + return Vector2d.ZERO + + val depth = intersectionDepth(other) + + if (depth.x.absoluteValue < depth.y.absoluteValue) { + return Vector2d(x = depth.x) + } else { + return Vector2d(y = depth.y) + } + } + + /** + * Рассчитывает "время" пересечения + * + * https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/swept-aabb-collision-detection-and-response-r3084/ + * + * Исправленный комментатором той же статьи от hypernewbie + */ + fun intersectionTime(other: AABB, velocity: Vector2d): IntersectionTime { + val xInvEntry: Double + val yInvEntry: Double + val xInvExit: Double + val yInvExit: Double + + if (velocity.x > 0.0) { + xInvEntry = other.mins.x - maxs.x + xInvExit = other.maxs.x - mins.x + } else { + xInvEntry = other.maxs.x - mins.x + xInvExit = other.mins.x - maxs.x + } + + if (velocity.y > 0.0) { + yInvEntry = other.mins.y - maxs.y + yInvExit = other.maxs.y - mins.y + } else { + yInvEntry = other.maxs.y - mins.y + yInvExit = other.mins.y - maxs.y + } + + var xEntry: Double + var yEntry: Double + val xExit: Double + val yExit: Double + + if (velocity.x == 0.0) { + xEntry = Double.NEGATIVE_INFINITY + xExit = Double.POSITIVE_INFINITY + } else { + xEntry = xInvEntry / velocity.x + xExit = xInvExit / velocity.x + } + + if (velocity.y == 0.0) { + yEntry = Double.NEGATIVE_INFINITY + yExit = Double.POSITIVE_INFINITY + } else { + yEntry = yInvEntry / velocity.y + yExit = yInvExit / velocity.y + } + + if (yEntry > 1.0) yEntry = Double.NEGATIVE_INFINITY + if (xEntry > 1.0) xEntry = Double.NEGATIVE_INFINITY + + return IntersectionTime( + Vector2d(xInvEntry, yInvEntry), + Vector2d(xInvExit, yInvExit), + Vector2d(xEntry, yEntry), + Vector2d(xExit, yExit), + ) + } + + /** + * Рассчитывает нормаль пересечения и процент пути ("время"), на котором произошло столкновение. + * + * Если столкновение не произошло, то возвращается [SweepResult.ZERO] + * + * Внимание: Если пересечение уже произошло (т.е. другой AABB пересекается с this), то данный метод + * вернёт заведомо ложный результат (т.е. "нет пересечения") + * + * https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/swept-aabb-collision-detection-and-response-r3084/ + * + * Исправленный комментатором той же статьи от hypernewbie + */ + fun sweep(other: AABB, velocity: Vector2d): SweepResult { + val time = intersectionTime(other, velocity) + val (near, far, entry, exit) = time + val (xEntry, yEntry) = entry + val (xExit, yExit) = exit + + val entryTime = xEntry.coerceAtLeast(yEntry) + val exitTime = xExit.coerceAtLeast(yExit) + + // гарантированно нет столкновения + if (entryTime > exitTime || xEntry < 0.0 && yEntry < 0.0) { + return SweepResult.ZERO + } + + if (xEntry < 0.0) { + if (maxs.x < other.mins.x || mins.x > other.maxs.x) + return SweepResult.ZERO + } + + if (yEntry < 0.0) { + if (maxs.y < other.mins.y || mins.y > other.maxs.y) + return SweepResult.ZERO + } + + val (xInvEntry, yInvEntry) = near + val normal: Vector2d + + if (xEntry > yEntry) { + if (xInvEntry < 0.0) { + normal = Vector2d.RIGHT + } else { + normal = Vector2d.LEFT + } + } else { + if (yInvEntry < 0.0) { + normal = Vector2d.UP + } else { + normal = Vector2d.DOWN + } + } + + return SweepResult(normal, entryTime, time) + } + + /** + * Рассчитывает нормаль пересечения и процент пути ("время"), на котором произошло столкновение. + * + * Если столкновение не произошло, то возвращается [SweepResult.ZERO] + * + * Если данный AABB уже столкнулся с [other], возвращается [SweepResult.INTERSECT] + * + * https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/swept-aabb-collision-detection-and-response-r3084/ + */ + fun safeSweep(other: AABB, velocity: Vector2d): SweepResult { + if (intersect(other)) { + return SweepResult.INTERSECT + } + + return sweep(other, velocity) + } + + fun encasingIntAABB(): AABBi { + return AABBi( + Vector2i(roundByAbsoluteValue(mins.x), roundByAbsoluteValue(mins.y)), + Vector2i(roundByAbsoluteValue(maxs.x), roundByAbsoluteValue(maxs.y)), + ) + } + + fun encasingChunkPosAABB(): AABBi { + return AABBi( + Vector2i(ChunkPos.tileToChunkComponent(roundByAbsoluteValue(mins.x)), ChunkPos.tileToChunkComponent(roundByAbsoluteValue(mins.y))), + Vector2i(ChunkPos.tileToChunkComponent(roundByAbsoluteValue(maxs.x)), ChunkPos.tileToChunkComponent(roundByAbsoluteValue(maxs.y))), + ) + } + + /** + * Возвращает AABB, который содержит в себе оба AABB + */ + fun combine(other: AABB): AABB { + val minX = mins.x.coerceAtMost(other.mins.x) + val minY = mins.y.coerceAtMost(other.mins.y) + val maxX = maxs.x.coerceAtLeast(other.maxs.x) + val maxY = maxs.y.coerceAtLeast(other.maxs.y) + + return AABB(Vector2d(minX, minY), Vector2d(maxX, maxY)) + } + + companion object { + fun rectangle(pos: IStruct2d, width: Double, height: Double = width): AABB { + val (x, y) = pos + + return AABB( + Vector2d(x - width / 2.0, y - height / 2.0), + Vector2d(x + width / 2.0, y + height / 2.0), + ) + } + } +} + +data class AABBi(val mins: Vector2i, val maxs: Vector2i) { + init { + require(mins.x <= maxs.x) { "mins.x ${mins.x} is more than maxs.x ${maxs.x}" } + require(mins.y <= maxs.y) { "mins.y ${mins.y} is more than maxs.y ${maxs.y}" } + } + + operator fun plus(other: AABBi) = AABBi(mins + other.mins, maxs + other.maxs) + operator fun minus(other: AABBi) = AABBi(mins - other.mins, maxs - other.maxs) + operator fun times(other: AABBi) = AABBi(mins * other.mins, maxs * other.maxs) + operator fun div(other: AABBi) = AABBi(mins / other.mins, maxs / other.maxs) + + operator fun plus(other: Vector2i) = AABBi(mins + other, maxs + other) + operator fun minus(other: Vector2i) = AABBi(mins - other, maxs - other) + operator fun times(other: Vector2i) = AABBi(mins * other, maxs * other) + operator fun div(other: Vector2i) = AABBi(mins / other, maxs / other) + + operator fun plus(other: Int) = AABBi(mins + other, maxs + other) + operator fun minus(other: Int) = AABBi(mins - other, maxs - other) + operator fun times(other: Int) = AABBi(mins * other, maxs * other) + operator fun div(other: Int) = AABBi(mins / other, maxs / other) + + val xSpan get() = maxs.x - mins.x + val ySpan get() = maxs.y - mins.y + val centre get() = mins.toDoubleVector() + maxs.toDoubleVector() * 0.5 + + val A get() = mins + val B get() = Vector2i(mins.x, maxs.y) + val C get() = maxs + val D get() = Vector2i(maxs.x, mins.y) + + val bottomLeft get() = A + val topLeft get() = B + val topRight get() = C + val bottomRight get() = D + + val width get() = (maxs.x - mins.x) / 2 + val height get() = (maxs.y - mins.y) / 2 + + val diameter get() = mins.distance(maxs) + val radius get() = diameter / 2.0 + + fun isInside(point: Vector2i): Boolean { + return point.x in mins.x .. maxs.x && point.y in mins.y .. maxs.y + } + + /** + * Есть ли пересечение между этим AABB и [other] + * + * Считается, что они пересекаются, даже если у них просто равна одна из осей + */ + fun intersect(other: AABBi): Boolean { + val intersectX: Boolean + + if (xSpan <= other.xSpan) + intersectX = mins.x in other.mins.x .. other.maxs.x || maxs.x in other.mins.x .. other.maxs.x + else + intersectX = other.mins.x in mins.x .. maxs.x || other.maxs.x in mins.x .. maxs.x + + if (!intersectX) + return false + + val intersectY: Boolean + + if (ySpan <= other.ySpan) + intersectY = mins.y in other.mins.y .. other.maxs.y || maxs.y in other.mins.y .. other.maxs.y + else + intersectY = other.mins.y in mins.y .. maxs.y || other.maxs.y in mins.y .. maxs.y + + return intersectY + } + + /** + * Есть ли пересечение между этим AABB и [other] + * + * Считается, что они НЕ пересекаются, если у них просто равна одна из осей + */ + fun intersectWeak(other: AABBi): Boolean { + if (maxs.x == other.mins.x || mins.x == other.maxs.x || maxs.y == other.mins.y || mins.y == other.maxs.y) + return false + + val intersectX: Boolean + + if (xSpan <= other.xSpan) + intersectX = mins.x in other.mins.x .. other.maxs.x || maxs.x in other.mins.x .. other.maxs.x + else + intersectX = other.mins.x in mins.x .. maxs.x || other.maxs.x in mins.x .. maxs.x + + if (!intersectX) + return false + + val intersectY: Boolean + + if (ySpan <= other.ySpan) + intersectY = mins.y in other.mins.y .. other.maxs.y || maxs.y in other.mins.y .. other.maxs.y + else + intersectY = other.mins.y in mins.y .. maxs.y || other.maxs.y in mins.y .. maxs.y + + return intersectY + } + + fun toDoubleAABB() = AABB(mins.toDoubleVector(), maxs.toDoubleVector()) + + private inner class Iterator(private val factory: (x: Int, y: Int) -> T) : kotlin.collections.Iterator { + private var x = mins.x + private var y = mins.y + private var next = true + + override fun hasNext(): Boolean { + return next + } + + override fun next(): T { + if (!next) + throw IllegalStateException() + + val obj = factory.invoke(x++, y) + + if (x > maxs.x) { + x = mins.x + + if (++y > maxs.y) { + next = false + } + } + + return obj + } + } + + val vectors: kotlin.collections.Iterator get() = Iterator(::Vector2i) + val chunkPositions: kotlin.collections.Iterator get() = Iterator(::ChunkPos) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Matrix.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Matrix.kt index 94b54c0b..47dd09ed 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Matrix.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Matrix.kt @@ -21,6 +21,10 @@ interface IMatrixLikeFloat : IMatrixLike { operator fun get(row: Int, column: Int): Float } +interface IMatrixLikeDouble : IMatrixLike { + operator fun get(row: Int, column: Int): Double +} + interface IMatrix : IMatrixLike { operator fun plus(other: IMatrix): IMatrix operator fun minus(other: IMatrix): IMatrix diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt new file mode 100644 index 00000000..393367ab --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt @@ -0,0 +1,27 @@ +package ru.dbotthepony.kstarbound.math + +fun lerp(t: Double, a: Double, b: Double): Double { + return a * (1.0 - t) + b * t +} + +/** + * Выполняет преобразование [value] типа [Double] в [Int] так, + * что выходной [Int] всегда будет больше или равен по модулю [value] + */ +fun roundByAbsoluteValue(value: Double): Int { + if (value > 0.0) { + if (value % 1.0 != 0.0) { + return value.toInt() + 1 + } + + return value.toInt() + } else if (value == -0.0 || value == 0.0) { + return 0 + } else { + if (value % 1.0 != -0.0) { + return value.toInt() - 1 + } + + return value.toInt() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt index 6ab73611..158ace54 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt @@ -1,12 +1,17 @@ package ru.dbotthepony.kstarbound.math import com.google.gson.JsonArray -import ru.dbotthepony.kstarbound.api.IStruct2f -import ru.dbotthepony.kstarbound.api.IStruct2i -import ru.dbotthepony.kstarbound.api.IStruct3f -import ru.dbotthepony.kstarbound.api.IStruct4f +import ru.dbotthepony.kstarbound.api.* import kotlin.math.cos +import kotlin.math.pow import kotlin.math.sin +import kotlin.math.sqrt + +// Так как у нас нет шаблонов ни в Java, ни в Kotlin +// а дженерики вызывают autoboxing +// приходится создавать "бетонные" реализации для каждого вида вектора + +// а ведь компилятор мог бы это генерировать. abstract class IVector2i> : IMatrixLike, IMatrixLikeInt, IStruct2i { override val columns = 1 @@ -20,13 +25,76 @@ abstract class IVector2i> : IMatrixLike, IMatrixLikeInt, IStruc operator fun times(other: IVector2i<*>) = make(x * other.x, y * other.y) operator fun div(other: IVector2i<*>) = make(x / other.x, y / other.y) + //operator fun plus(other: IVector2f<*>) = Vector2f(x + other.x, y + other.y) + //operator fun minus(other: IVector2f<*>) = Vector2f(x - other.x, y - other.y) + //operator fun times(other: IVector2f<*>) = Vector2f(x * other.x, y * other.y) + //operator fun div(other: IVector2f<*>) = Vector2f(x / other.x, y / other.y) + + //operator fun plus(other: IVector2d<*>) = Vector2d(x + other.x, y + other.y) + //operator fun minus(other: IVector2d<*>) = Vector2d(x - other.x, y - other.y) + //operator fun times(other: IVector2d<*>) = Vector2d(x * other.x, y * other.y) + //operator fun div(other: IVector2d<*>) = Vector2d(x / other.x, y / other.y) + operator fun div(other: Int) = make(x / other, y / other) operator fun times(other: Int) = make(x * other, y * other) operator fun minus(other: Int) = make(x - other, y - other) operator fun plus(other: Int) = make(x + other, y + other) + //operator fun div(other: Float) = Vector2f(x / other, y / other) + //operator fun times(other: Float) = Vector2f(x * other, y * other) + //operator fun minus(other: Float) = Vector2f(x - other, y - other) + //operator fun plus(other: Float) = Vector2f(x + other, y + other) + + //operator fun div(other: Double) = Vector2d(x / other, y / other) + //operator fun times(other: Double) = Vector2d(x * other, y * other) + //operator fun minus(other: Double) = Vector2d(x - other, y - other) + //operator fun plus(other: Double) = Vector2d(x + other, y + other) + operator fun unaryMinus() = make(-x, -y) + val length get() = sqrt(x.toDouble() * x.toDouble() + y.toDouble() * y.toDouble()) + + fun dotProduct(other: IVector2i<*>): Double { + return other.x.toDouble() * x.toDouble() + other.y.toDouble() * y.toDouble() + } + + fun dotProduct(other: IVector2f<*>): Double { + return other.x.toDouble() * x.toDouble() + other.y.toDouble() * y.toDouble() + } + + fun dotProduct(other: IVector2d<*>): Double { + return other.x * x.toDouble() + other.y * y.toDouble() + } + + fun InvDotProduct(other: IVector2i<*>): Double { + return other.x.toDouble() * y.toDouble() + other.y.toDouble() * x.toDouble() + } + + fun InvDotProduct(other: IVector2f<*>): Double { + return other.x.toDouble() * y.toDouble() + other.y.toDouble() * x.toDouble() + } + + fun InvDotProduct(other: IVector2d<*>): Double { + return other.x * y.toDouble() + other.y * x.toDouble() + } + + fun distance(other: IVector2i<*>): Double { + return sqrt((x - other.x).toDouble().pow(2.0) + (y - other.y).toDouble().pow(2.0)) + } + + fun distance(other: IVector2f<*>): Double { + return sqrt((x - other.x).toDouble().pow(2.0) + (y - other.y).toDouble().pow(2.0)) + } + + fun distance(other: IVector2d<*>): Double { + return sqrt((x - other.x).toDouble().pow(2.0) + (y - other.y).toDouble().pow(2.0)) + } + + val normalized: Vector2d get() { + val len = length + return Vector2d(x / len, y / len) + } + fun left() = make(x - 1, y) fun right() = make(x + 1, y) fun up() = make(x, y + 1) @@ -45,6 +113,9 @@ abstract class IVector2i> : IMatrixLike, IMatrixLikeInt, IStruc } protected abstract fun make(x: Int, y: Int): T + + fun toFloatVector() = Vector2f(x.toFloat(), y.toFloat()) + fun toDoubleVector() = Vector2d(x.toDouble(), y.toDouble()) } data class Vector2i(override val x: Int = 0, override val y: Int = 0) : IVector2i() { @@ -101,6 +172,49 @@ abstract class IVector2f> : IMatrixLike, IMatrixLikeFloat, IStr fun up() = make(x, y + 1) fun down() = make(x, y - 1) + val length get() = sqrt(x.toDouble() * x.toDouble() + y.toDouble() * y.toDouble()) + + fun dotProduct(other: IVector2i<*>): Double { + return other.x.toDouble() * x.toDouble() + other.y.toDouble() * y.toDouble() + } + + fun dotProduct(other: IVector2f<*>): Double { + return other.x.toDouble() * x.toDouble() + other.y.toDouble() * y.toDouble() + } + + fun dotProduct(other: IVector2d<*>): Double { + return other.x * x.toDouble() + other.y * y.toDouble() + } + + fun InvDotProduct(other: IVector2i<*>): Double { + return other.x.toDouble() * y.toDouble() + other.y.toDouble() * x.toDouble() + } + + fun InvDotProduct(other: IVector2f<*>): Double { + return other.x.toDouble() * y.toDouble() + other.y.toDouble() * x.toDouble() + } + + fun InvDotProduct(other: IVector2d<*>): Double { + return other.x * y.toDouble() + other.y * x.toDouble() + } + + fun distance(other: IVector2i<*>): Double { + return sqrt((x - other.x).toDouble().pow(2.0) + (y - other.y).toDouble().pow(2.0)) + } + + fun distance(other: IVector2f<*>): Double { + return sqrt((x - other.x).toDouble().pow(2.0) + (y - other.y).toDouble().pow(2.0)) + } + + fun distance(other: IVector2d<*>): Double { + return sqrt((x - other.x).toDouble().pow(2.0) + (y - other.y).toDouble().pow(2.0)) + } + + val normalized: Vector2d get() { + val len = length + return Vector2d(x / len, y / len) + } + override fun get(row: Int, column: Int): Float { if (column != 0) { throw IndexOutOfBoundsException("Column must be 0 ($column given)") @@ -114,6 +228,8 @@ abstract class IVector2f> : IMatrixLike, IMatrixLikeFloat, IStr } protected abstract fun make(x: Float, y: Float): T + + fun toDoubleVector() = Vector2d(x.toDouble(), y.toDouble()) } data class Vector2f(override val x: Float = 0f, override val y: Float = 0f) : IVector2f() { @@ -146,6 +262,122 @@ data class MutableVector2f(override var x: Float = 0f, override var y: Float = 0 } } +abstract class IVector2d> : IMatrixLike, IMatrixLikeDouble, IStruct2d { + override val columns = 1 + override val rows = 2 + + abstract val x: Double + abstract val y: Double + + operator fun plus(other: IVector2d<*>) = make(x + other.x, y + other.y) + operator fun minus(other: IVector2d<*>) = make(x - other.x, y - other.y) + operator fun times(other: IVector2d<*>) = make(x * other.x, y * other.y) + operator fun div(other: IVector2d<*>) = make(x / other.x, y / other.y) + + operator fun plus(other: Double) = make(x + other, y + other) + operator fun minus(other: Double) = make(x - other, y - other) + operator fun times(other: Double) = make(x * other, y * other) + operator fun div(other: Double) = make(x / other, y / other) + + operator fun unaryMinus() = make(-x, -y) + + val length get() = sqrt(x * x + y * y) + + fun dotProduct(other: IVector2i<*>): Double { + return other.x * x + other.y * y + } + + fun dotProduct(other: IVector2f<*>): Double { + return other.x * x + other.y * y + } + + fun dotProduct(other: IVector2d<*>): Double { + return other.x * x + other.y * y + } + + fun invDotProduct(other: IVector2i<*>): Double { + return other.x * y + other.y * x + } + + fun invDotProduct(other: IVector2f<*>): Double { + return other.x * y + other.y * x + } + + fun invDotProduct(other: IVector2d<*>): Double { + return other.x * y + other.y * x + } + + fun distance(other: IVector2i<*>): Double { + return sqrt((x - other.x).toDouble().pow(2.0) + (y - other.y).toDouble().pow(2.0)) + } + + fun distance(other: IVector2f<*>): Double { + return sqrt((x - other.x).toDouble().pow(2.0) + (y - other.y).toDouble().pow(2.0)) + } + + fun distance(other: IVector2d<*>): Double { + return sqrt((x - other.x).toDouble().pow(2.0) + (y - other.y).toDouble().pow(2.0)) + } + + val normalized: Vector2d get() { + val len = length + return Vector2d(x / len, y / len) + } + + fun left() = make(x - 1, y) + fun right() = make(x + 1, y) + fun up() = make(x, y + 1) + fun down() = make(x, y - 1) + + override fun get(row: Int, column: Int): Double { + 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") + } + } + + protected abstract fun make(x: Double, y: Double): T +} + +data class Vector2d(override val x: Double = 0.0, override val y: Double = 0.0) : IVector2d() { + override fun make(x: Double, y: Double) = Vector2d(x, y) + + companion object { + fun fromJson(input: JsonArray): Vector2d { + return Vector2d(input[0].asDouble, input[1].asDouble) + } + + val ZERO = Vector2d() + val LEFT = Vector2d().left() + val RIGHT = Vector2d().right() + val UP = Vector2d().up() + val DOWN = Vector2d().down() + + val INVERT_X = Vector2d(-1.0, 1.0) + val INVERT_Y = Vector2d(1.0, -1.0) + val INVERT_XY = Vector2d(-1.0, -1.0) + } +} + +data class MutableVector2d(override var x: Double = 0.0, override var y: Double = 0.0) : IVector2d() { + override fun make(x: Double, y: Double): MutableVector2d { + this.x = x + this.y = y + return this + } + + companion object { + fun fromJson(input: JsonArray): MutableVector2d { + return MutableVector2d(input[0].asDouble, input[1].asDouble) + } + } +} + abstract class IVector3f> : IMatrixLike, IMatrixLikeFloat, IStruct3f { override val columns = 1 override val rows = 3 @@ -166,6 +398,12 @@ abstract class IVector3f> : IMatrixLike, IMatrixLikeFloat, IStr operator fun unaryMinus() = make(-x, -y, -z) + val length get() = sqrt(x.toDouble() * x.toDouble() + y.toDouble() * y.toDouble() + z.toDouble() * z.toDouble()) + + fun dotProduct(other: IVector3f<*>): Double { + return other.x.toDouble() * x.toDouble() + other.y.toDouble() * y.toDouble() + other.z.toDouble() * z.toDouble() + } + override fun get(row: Int, column: Int): Float { if (column != 0) { throw IndexOutOfBoundsException("Column must be 0 ($column given)") @@ -344,3 +582,85 @@ data class MutableVector4f(override var x: Float = 0f, override var y: Float = 0 return this } } + +abstract class IVector4d> : IMatrixLike, IMatrixLikeDouble, IStruct4d { + abstract val x: Double + abstract val y: Double + abstract val z: Double + abstract val w: Double + + operator fun plus(other: IVector4f<*>) = make(x + other.x, y + other.y, z + other.z, w + other.w) + operator fun minus(other: IVector4f<*>) = make(x - other.x, y - other.y, z - other.z, w + other.w) + operator fun times(other: IVector4f<*>) = make(x * other.x, y * other.y, z * other.z, w + other.w) + operator fun div(other: IVector4f<*>) = make(x / other.x, y / other.y, z / other.z, w + other.w) + + operator fun plus(other: Double) = make(x + other, y + other, z + other, w + other) + operator fun minus(other: Double) = make(x - other, y - other, z - other, w - other) + operator fun times(other: Double) = make(x * other, y * other, z * other, w * other) + operator fun div(other: Double) = make(x / other, y / other, z / other, w / other) + + operator fun unaryMinus() = make(-x, -y, -z, -w) + + override val columns = 1 + override val rows = 4 + + override fun get(row: Int, column: Int): Double { + 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: IMatrixLikeDouble): T { + 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 make(x, y, z, w) + } + + throw IllegalArgumentException("Incompatible matrix provided: ${other.rows} x ${other.columns}") + } + + protected abstract fun make(x: Double, y: Double, z: Double, w: Double): T +} + +data class Vector4d(override val x: Double = 0.0, override val y: Double = 0.0, override val z: Double = 0.0, override val w: Double = 0.0) : IVector4d() { + override fun make(x: Double, y: Double, z: Double, w: Double): Vector4d { + return Vector4d(x, y, z, w) + } +} + +data class MutableVector4d(override var x: Double = 0.0, override var y: Double = 0.0, override var z: Double = 0.0, override var w: Double = 0.0) : IVector4d() { + override fun make(x: Double, y: Double, z: Double, w: Double): MutableVector4d { + this.x = x + this.y = y + this.z = z + this.w = w + return this + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Formatter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Formatter.kt index 8bd718e1..f1836c40 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Formatter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Formatter.kt @@ -9,8 +9,8 @@ private const val PETIBYTE = TEBIBYTE * 1024L fun formatBytesShort(input: Long): String { return when (input) { in 0 until KIBIBYTE -> "${input}b" - in KIBIBYTE until MEBIBYTE -> "%.2fKiB".format((input / KIBIBYTE).toDouble() + (input % KIBIBYTE).toDouble() / KIBIBYTE) - in MEBIBYTE until GIBIBYTE -> "%.2fMiB".format((input / MEBIBYTE).toDouble() + (input % MEBIBYTE).toDouble() / MEBIBYTE) + in KIBIBYTE until MEBIBYTE -> "${(((input / KIBIBYTE).toDouble() + (input % KIBIBYTE).toDouble() / KIBIBYTE) * 100.0).toLong().toDouble() / 100.0}KiB" + in MEBIBYTE until GIBIBYTE -> "${(((input / MEBIBYTE).toDouble() + (input % MEBIBYTE).toDouble() / MEBIBYTE) * 100.0).toLong().toDouble() / 100.0}MiB" else -> "${input}b" } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index 3aa2958d..fae159c2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -2,8 +2,12 @@ package ru.dbotthepony.kstarbound.world import ru.dbotthepony.kstarbound.api.IStruct2i import ru.dbotthepony.kstarbound.defs.TileDefinition +import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.IVector2i +import ru.dbotthepony.kstarbound.math.Vector2d import ru.dbotthepony.kstarbound.math.Vector2i +import java.util.* +import kotlin.collections.ArrayList /** * Представляет из себя класс, который содержит состояние тайла на заданной позиции @@ -146,7 +150,10 @@ interface IMutableTileChunk : ITileChunk, ITileSetter const val CHUNK_SHIFT = 5 const val CHUNK_SIZE = 1 shl CHUNK_SHIFT // 32 + const val CHUNK_SIZE_FF = CHUNK_SIZE - 1 +const val CHUNK_SIZEf = CHUNK_SIZE.toFloat() +const val CHUNK_SIZEd = CHUNK_SIZE.toDouble() data class ChunkPos(override val x: Int, override val y: Int) : IVector2i() { constructor(pos: IStruct2i) : this(pos.component1(), pos.component2()) @@ -158,7 +165,25 @@ data class ChunkPos(override val x: Int, override val y: Int) : IVector2i?, val pos: ChunkPos) { changeset++ } + val aabb = aabbBase + Vector2d(pos.x * CHUNK_SIZE.toDouble(), pos.y * CHUNK_SIZE.toDouble()) + inner class TileLayer : IMutableTileChunk { /** * Возвращает счётчик изменений этого слоя @@ -306,6 +333,65 @@ open class Chunk(val world: World<*>?, val pos: ChunkPos) { this@Chunk.changeset++ } + private val collisionCache = ArrayList() + private val collisionCacheView = Collections.unmodifiableCollection(collisionCache) + private var collisionChangeset = -1 + + private fun bakeCollisions() { + collisionChangeset = changeset + val seen = BooleanArray(tiles.size) + + collisionCache.clear() + + val xAdd = pos.x * CHUNK_SIZEd + val yAdd = pos.y * CHUNK_SIZEd + + for (y in 0 .. CHUNK_SIZE_FF) { + var first: Int? = null + var last = 0 + + for (x in 0 .. CHUNK_SIZE_FF) { + if (tiles[x or (y shl CHUNK_SHIFT)] != null) { + if (first == null) { + first = x + } + + last = x + } else { + if (first != null) { + collisionCache.add(AABB( + Vector2d(x = xAdd + first.toDouble(), y = y.toDouble() + yAdd), + Vector2d(x = xAdd + last.toDouble() + 1.0, y = y.toDouble() + 1.0 + yAdd), + )) + + first = null + } + } + } + + if (first != null) { + collisionCache.add(AABB( + Vector2d(x = first.toDouble() + xAdd, y = y.toDouble() + yAdd), + Vector2d(x = last.toDouble() + 1.0 + xAdd, y = y.toDouble() + 1.0 + yAdd), + )) + } + } + } + + /** + * Возвращает список AABB тайлов этого слоя + * + * Данный список напрямую указывает на внутреннее состояние и будет изменён при перестройке + * коллизии чанка, поэтому если необходим стабильный список, его необходимо скопировать + */ + fun collisionLayers(): Collection { + if (collisionChangeset != changeset) { + bakeCollisions() + } + + return collisionCacheView + } + override val pos: ChunkPos get() = this@Chunk.pos @@ -354,5 +440,10 @@ open class Chunk(val world: World<*>?, val pos: ChunkPos) { override fun get(x: Int, y: Int): ChunkTile? = null override fun set(x: Int, y: Int, tile: TileDefinition?): ChunkTile? = null } + + private val aabbBase = AABB( + Vector2d.ZERO, + Vector2d(CHUNK_SIZE.toDouble(), CHUNK_SIZE.toDouble()), + ) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index eb08aad1..2a3f1a3e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -1,6 +1,9 @@ package ru.dbotthepony.kstarbound.world +import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT import ru.dbotthepony.kstarbound.defs.TileDefinition +import ru.dbotthepony.kstarbound.math.AABBi +import ru.dbotthepony.kstarbound.math.Vector2d import ru.dbotthepony.kstarbound.math.Vector2i /** @@ -40,11 +43,19 @@ open class MutableWorldChunkTuple( override var bottom: IWorldChunkTuple?, ) : IMutableWorldChunkTuple -@Suppress("WeakerAccess") +const val EARTH_FREEFALL_ACCELERATION = 9.8312 / METRES_IN_STARBOUND_UNIT + abstract class World(val seed: Long = 0L) { protected val chunkMap = HashMap() protected var lastAccessedChunk: T? = null + /** + * Стандартное ускорение свободного падения в Starbound Units/секунда^2 + * + * При Vector2d.ZERO = невесомость + */ + var gravity = Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION) + protected abstract fun tupleFactory( chunk: Chunk, top: IWorldChunkTuple?, @@ -155,22 +166,56 @@ abstract class World(val seed: Long = 0L) { } fun getTile(pos: Vector2i): ChunkTile? { - return getChunkInternal(ChunkPos.fromTilePosition(pos))?.chunk?.foreground?.get(pos.x, pos.y) + return getChunkInternal(ChunkPos.fromTilePosition(pos))?.chunk?.foreground?.get(ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y)) } fun setTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple { val chunk = computeIfAbsentInternal(ChunkPos.fromTilePosition(pos)) - chunk.chunk.foreground[pos.x and CHUNK_SIZE_FF, pos.y and CHUNK_SIZE_FF] = tile + chunk.chunk.foreground[ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y)] = tile return chunk } fun getBackgroundTile(pos: Vector2i): ChunkTile? { - return getChunkInternal(ChunkPos.fromTilePosition(pos))?.chunk?.background?.get(pos.x, pos.y) + return getChunkInternal(ChunkPos.fromTilePosition(pos))?.chunk?.background?.get(ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y)) } fun setBackgroundTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple { val chunk = computeIfAbsentInternal(ChunkPos.fromTilePosition(pos)) - chunk.chunk.background[pos.x and CHUNK_SIZE_FF, pos.y and CHUNK_SIZE_FF] = tile + chunk.chunk.background[ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y)] = tile return chunk } + + protected open fun collectInternal(boundingBox: AABBi): List { + val output = ArrayList() + + for (pos in boundingBox.chunkPositions) { + val chunk = getChunkInternal(pos) + + if (chunk != null) { + output.add(chunk) + } + } + + return output + } + + /** + * Возвращает все чанки, которые пересекаются с заданным [boundingBox] + */ + open fun collect(boundingBox: AABBi): List { + val output = ArrayList() + + for (chunk in collectInternal(boundingBox)) { + output.add(WorldChunkTuple( + world = chunk.world, + chunk = chunk.chunk, + top = chunk.top, + left = chunk.left, + right = chunk.right, + bottom = chunk.bottom, + )) + } + + return output + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt new file mode 100644 index 00000000..106119f0 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt @@ -0,0 +1,8 @@ +package ru.dbotthepony.kstarbound.world.entities + +import ru.dbotthepony.kstarbound.world.World + +open class AliveEntity(world: World<*>) : Entity(world) { + var maxHealth = 10.0 + var health = 10.0 +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt new file mode 100644 index 00000000..bb925bda --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt @@ -0,0 +1,179 @@ +package ru.dbotthepony.kstarbound.world.entities + +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kstarbound.math.AABB +import ru.dbotthepony.kstarbound.math.Vector2d +import ru.dbotthepony.kstarbound.math.lerp +import ru.dbotthepony.kstarbound.world.Chunk +import ru.dbotthepony.kstarbound.world.World +import kotlin.math.absoluteValue + +enum class CollisionResolution { + STOP, + BOUNCE, + PUSH, + SLIDE, +} + +/** + * Определяет из себя сущность в мире, которая имеет позицию, скорость и коробку столкновений + */ +open class Entity(val world: World<*>) { + var chunk: Chunk? = null + protected set + + val worldaabb get() = aabb + pos + var pos = Vector2d() + var velocity = Vector2d() + + /** + * Касается ли сущность земли + * + * Данный флаг выставляется при обработке скорости, если данный флаг не будет выставлен + * правильно, то сущность будет иметь очень плохое движение в стороны + * + * Так же от него зависит то, может ли сущность двигаться, если она не парит + * + * Если сущность касается земли, то на неё не действует гравитация + */ + var onGround = false + protected set + + // наследуемые свойства + open val aabb = AABB.rectangle(Vector2d.ZERO, 0.9, 0.9) + open val affectedByGravity = true + open val collisionResolution = CollisionResolution.STOP + + protected open fun onTouchGround(velocity: Vector2d, normal: Vector2d) { + + } + + open fun propagateVelocity(delta: Double) { + if (velocity.length == 0.0) + return + + var deltaMovement = velocity * delta + + var potentialAABB = worldaabb + deltaMovement + var combined = worldaabb.combine(potentialAABB) + val collected = world.collect((combined).encasingChunkPosAABB()).map { it.chunk.foreground.collisionLayers() } + + if (collected.isNotEmpty()) { + var newOnGround = false + + for (iteration in 0 .. 100) { + var collided = false + + for (aabbList in collected) { + for (aabb in aabbList) { + if (!newOnGround && iteration == 0) { + if (worldaabb.sweep(aabb, world.gravity * delta).collisionTime < 1.0) { + newOnGround = true + } + } + + // залез в блоки? + if (potentialAABB.intersectWeak(aabb)) { + val push = worldaabb.pushOutFrom(aabb) + + if (push.length > 0.0) { + velocity -= push * delta * 100.0 + deltaMovement = velocity * delta + potentialAABB = worldaabb + deltaMovement + combined = worldaabb.combine(potentialAABB) + + onGround = true + collided = true + continue + } + } + + // ранний тест (отсечение заведомо не пересекаемой геометрии) + if (!aabb.intersect(combined)) { + continue + } + + // обычный тест коллизии + val (normal, collisionTime) = worldaabb.sweep(aabb, deltaMovement) + + if (collisionTime != 1.0) { + val remainingTime = 1.0 - collisionTime + val oldVelocity = velocity + + when (collisionResolution) { + CollisionResolution.STOP -> { + velocity *= remainingTime + } + + CollisionResolution.PUSH -> { + var dot = deltaMovement.invDotProduct(normal) + val magnitude = deltaMovement.length * remainingTime + + if (dot > 0.0) { + dot = 1.0 + } else { + dot = -1.0 + } + + velocity = Vector2d(dot * normal.y * magnitude, dot * normal.x * magnitude) / delta + } + + CollisionResolution.SLIDE -> { + val dot = deltaMovement.invDotProduct(normal) * remainingTime + velocity = Vector2d(dot * normal.y, dot * normal.x) / delta + } + + CollisionResolution.BOUNCE -> { + velocity *= remainingTime + + if (normal.x.absoluteValue > 0.00001 && normal.y.absoluteValue > 0.00001) { + velocity *= Vector2d.INVERT_XY + } else if (normal.x.absoluteValue > 0.00001) { + velocity *= Vector2d.INVERT_X + } else if (normal.y.absoluteValue > 0.00001) { + velocity *= Vector2d.INVERT_Y + } + } + } + + collided = true + + if (!newOnGround) { + newOnGround = normal.dotProduct(world.gravity) <= -0.98 + } + + deltaMovement = velocity * delta + potentialAABB = worldaabb + deltaMovement + onTouchGround(oldVelocity, normal) + } + } + } + + if (!collided) { + //println("Resolved collision on $iteration") + break + } + } + + onGround = newOnGround + //println(newOnGround) + } else { + onGround = false + } + + pos += velocity * delta + } + + open fun moveAndCollide(delta: Double) { + if (!onGround && affectedByGravity) + velocity += world.gravity * delta + else if (affectedByGravity) + velocity *= Vector2d(lerp(delta, 1.0, 0.01), 1.0) + + propagateVelocity(delta) + } + + companion object { + private val LOGGER = LogManager.getLogger(Entity::class.java) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Humanoid.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Humanoid.kt new file mode 100644 index 00000000..41258bb1 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Humanoid.kt @@ -0,0 +1,10 @@ +package ru.dbotthepony.kstarbound.world.entities + +import ru.dbotthepony.kstarbound.math.AABB +import ru.dbotthepony.kstarbound.math.Vector2d +import ru.dbotthepony.kstarbound.world.World + +open class Humanoid(world: World<*>) : AliveEntity(world) { + override val aabb = AABB.rectangle(Vector2d.ZERO, 1.8, 3.7) + override val collisionResolution = CollisionResolution.SLIDE +} diff --git a/src/main/resources/shaders/fragment/flat_color.glsl b/src/main/resources/shaders/fragment/flat_color.glsl new file mode 100644 index 00000000..809cabd8 --- /dev/null +++ b/src/main/resources/shaders/fragment/flat_color.glsl @@ -0,0 +1,9 @@ + +#version 460 + +uniform vec4 _color; +out vec4 _color_out; + +void main() { + _color_out = _color; +} diff --git a/src/main/resources/shaders/vertex/flat_vertex_2d.glsl b/src/main/resources/shaders/vertex/flat_vertex_2d.glsl new file mode 100644 index 00000000..dfec2e1f --- /dev/null +++ b/src/main/resources/shaders/vertex/flat_vertex_2d.glsl @@ -0,0 +1,9 @@ + +#version 460 + +layout (location = 0) in vec2 _pos; +uniform mat4 _transform; + +void main() { + gl_Position = _transform * vec4(_pos, 0.5, 1.0); +} diff --git a/src/test/kotlin/ru/dbotthepony/kstarbound/test/MathTests.kt b/src/test/kotlin/ru/dbotthepony/kstarbound/test/MathTests.kt new file mode 100644 index 00000000..fb1b7089 --- /dev/null +++ b/src/test/kotlin/ru/dbotthepony/kstarbound/test/MathTests.kt @@ -0,0 +1,51 @@ +package ru.dbotthepony.kstarbound.test + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import ru.dbotthepony.kstarbound.math.AABB +import ru.dbotthepony.kstarbound.math.Vector2d +import ru.dbotthepony.kstarbound.math.roundByAbsoluteValue + +object MathTests { + @Test + @DisplayName("roundByAbsoluteValue test") + fun roundByAbsoluteValueTest() { + check(roundByAbsoluteValue(0.0) == 0) + check(roundByAbsoluteValue(0.1) == 1) + check(roundByAbsoluteValue(1.1) == 2) + check(roundByAbsoluteValue(-0.1) == -1) + check(roundByAbsoluteValue(-0.0) == 0) + check(roundByAbsoluteValue(-1.0) == -1) + check(roundByAbsoluteValue(-1.1) == -2) + } + + @Test + @DisplayName("AABB Basic Math") + fun basicAABB() { + val a = AABB.rectangle(Vector2d.ZERO, 1.0, 1.0) + + check(a.intersect(AABB.rectangle(Vector2d(-1.0), 1.0, 1.0))) + check(!a.intersectWeak(AABB.rectangle(Vector2d(-1.0), 1.0, 1.0))) + check(!a.intersect(AABB.rectangle(Vector2d(-2.0), 1.0, 1.0))) + check(!a.intersectWeak(AABB.rectangle(Vector2d(-2.0), 1.0, 1.0))) + + check(a.intersect(AABB.rectangle(Vector2d(-0.9), 1.0, 1.0))) + check(a.intersectWeak(AABB.rectangle(Vector2d(-0.9), 1.0, 1.0))) + + val bigA = AABB.rectangle(Vector2d.ZERO, 200.0, 200.0) + val smallB = AABB.rectangle(Vector2d.ZERO, 1.0, 1.0) + + check(bigA.intersect(smallB)) + check(smallB.intersect(bigA)) + + check(bigA.intersectWeak(smallB)) + check(smallB.intersectWeak(bigA)) + + check(AABB.rectangle(Vector2d.ZERO, 1.0, 1.0) == AABB(Vector2d(-0.5, -0.5), Vector2d(0.5, 0.5))) + + val combineA = AABB(Vector2d(0.0, 0.0), Vector2d(2.0, 2.0)) + val combineB = AABB(Vector2d(2.0, 5.0), Vector2d(4.0, 6.0)) + + check(combineA.combine(combineB) == AABB(Vector2d(0.0, 0.0), Vector2d(4.0, 6.0))) { combineA.combine(combineB).toString() } + } +}