Ну мы идем дальше

обрезка геометрии мира
тест коллизий
сущности
This commit is contained in:
DBotThePony 2022-02-06 01:00:40 +07:00
parent c045a699d4
commit 5fe7668fe5
Signed by: DBot
GPG Key ID: DCC23B5715498507
28 changed files with 1852 additions and 191 deletions

View File

@ -2,22 +2,13 @@ package ru.dbotthepony.kstarbound
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.lwjgl.Version import org.lwjgl.Version
import org.lwjgl.glfw.GLFW.*
import org.lwjgl.opengl.GL46.*
import ru.dbotthepony.kstarbound.client.StarboundClient 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.defs.TileDefinition
import ru.dbotthepony.kstarbound.util.Color import ru.dbotthepony.kstarbound.math.Vector2d
import ru.dbotthepony.kstarbound.util.formatBytesShort
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.entities.Humanoid
import java.io.File import java.io.File
import java.util.*
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
@ -41,43 +32,38 @@ fun main() {
Starbound.onInitialize { Starbound.onInitialize {
chunkA = client.world!!.computeIfAbsent(ChunkPos(0, 0)).chunk chunkA = client.world!!.computeIfAbsent(ChunkPos(0, 0)).chunk
val chunkB = client.world!!.computeIfAbsent(ChunkPos(-1, 0)).chunk val chunkB = client.world!!.computeIfAbsent(ChunkPos(-1, 0)).chunk
val chunkC = client.world!!.computeIfAbsent(ChunkPos(-2, 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 tile = Starbound.getTileDefinition("alienrock") val tile = Starbound.getTileDefinition("alienrock")
for (x in 0 .. 31) { for (x in -48 .. 48) {
for (y in 0 .. 31) { for (y in 0 .. 20) {
chunkA!!.foreground[x, y] = tile 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 (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 chunkB.foreground[x, y] = tile
} }
} }
@ -87,16 +73,44 @@ fun main() {
chunkA!!.foreground[x, y] = null as TileDefinition? 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()) { while (client.renderFrame()) {
Starbound.pollCallbacks() Starbound.pollCallbacks()
if (chunkA != null && glfwGetTime() < 10.0) { ent.moveAndCollide(client.frameRenderTime)
val tile = Starbound.getTileDefinition("alienrock") 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 //chunkA!!.foreground[rand.nextInt(0, CHUNK_SIZE_FF), rand.nextInt(0, CHUNK_SIZE_FF)] = tile
} //}
} }
} }

View File

@ -9,8 +9,8 @@ import ru.dbotthepony.kstarbound.world.World
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
const val METRES_IN_STARBOUND_UNIT = 0.5 const val METRES_IN_STARBOUND_UNIT = 0.25
const val METRES_IN_STARBOUND_UNITf = 0.5f const val METRES_IN_STARBOUND_UNITf = 0.25f
const val PIXELS_IN_STARBOUND_UNIT = 8.0 const val PIXELS_IN_STARBOUND_UNIT = 8.0
const val PIXELS_IN_STARBOUND_UNITf = 8.0f const val PIXELS_IN_STARBOUND_UNITf = 8.0f

View File

@ -6,7 +6,7 @@ data class ClientSettings(
* *
* Масштаб в единицу означает что один Starbound Unit будет равен 8 пикселям на экране * Масштаб в единицу означает что один Starbound Unit будет равен 8 пикселям на экране
*/ */
var scale: Float = 2f var scale: Float = 2f,
) {
} var debugCollisions: Boolean = true,
)

View File

@ -1,12 +1,12 @@
package ru.dbotthepony.kstarbound.client package ru.dbotthepony.kstarbound.client
import ru.dbotthepony.kstarbound.api.IStruct2d
import ru.dbotthepony.kstarbound.api.IStruct2f import ru.dbotthepony.kstarbound.api.IStruct2f
import ru.dbotthepony.kstarbound.client.render.ChunkRenderer import ru.dbotthepony.kstarbound.client.render.ChunkRenderer
import ru.dbotthepony.kstarbound.client.render.renderLayeredList import ru.dbotthepony.kstarbound.client.render.renderLayeredList
import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.world.IWorldChunkTuple import ru.dbotthepony.kstarbound.math.Vector2d
import ru.dbotthepony.kstarbound.world.MutableWorldChunkTuple import ru.dbotthepony.kstarbound.world.*
import ru.dbotthepony.kstarbound.world.World
class ClientWorldChunkTuple( class ClientWorldChunkTuple(
world: World<*>, world: World<*>,
@ -47,7 +47,7 @@ class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWo
} }
/** /**
* Отрисовывает этот мир с точки зрения [pos] в Starbound Units * Отрисовывает этот с обрезкой невидимой геометрии с точки зрения [size] в Starbound Units
* *
* Все координаты "местности" сохраняются, поэтому, если отрисовывать слишком далеко от 0, 0 * Все координаты "местности" сохраняются, поэтому, если отрисовывать слишком далеко от 0, 0
* то геометрия может начать искажаться из-за погрешности плавающей запятой * то геометрия может начать искажаться из-за погрешности плавающей запятой
@ -55,25 +55,22 @@ class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWo
* Обрезает всю заведомо невидимую геометрию на основе аргументов mins и maxs (в пикселях) * Обрезает всю заведомо невидимую геометрию на основе аргументов mins и maxs (в пикселях)
*/ */
fun render( fun render(
pos: IStruct2f, size: AABB,
scale: Float = 1f,
mins: IStruct2f,
maxs: IStruct2f,
) { ) {
val determineRenderers = ArrayList<ChunkRenderer>() val determineRenderers = ArrayList<ChunkRenderer>()
for (chunk in chunkMap.values) { for (chunk in collectInternal(size.encasingChunkPosAABB())) {
determineRenderers.add(chunk.renderer) determineRenderers.add(chunk.renderer)
} }
val renderList = ArrayList<ChunkRenderer>()
for (renderer in determineRenderers) { for (renderer in determineRenderers) {
renderList.add(renderer)
renderer.autoBakeStatic() renderer.autoBakeStatic()
} }
renderLayeredList(client.gl.matrixStack, renderList) renderLayeredList(client.gl.matrixStack, determineRenderers)
for (renderer in determineRenderers) {
renderer.renderDebug()
}
} }
} }

View File

@ -7,11 +7,15 @@ import org.lwjgl.glfw.GLFWErrorCallback
import org.lwjgl.opengl.GL46.* import org.lwjgl.opengl.GL46.*
import org.lwjgl.system.MemoryStack import org.lwjgl.system.MemoryStack
import org.lwjgl.system.MemoryUtil 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.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.math.Matrix4f import ru.dbotthepony.kstarbound.math.Matrix4f
import ru.dbotthepony.kstarbound.client.render.Camera import ru.dbotthepony.kstarbound.client.render.Camera
import ru.dbotthepony.kstarbound.client.render.TextAlignX import ru.dbotthepony.kstarbound.client.render.TextAlignX
import ru.dbotthepony.kstarbound.client.render.TextAlignY 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.math.Vector2f
import ru.dbotthepony.kstarbound.util.Color import ru.dbotthepony.kstarbound.util.Color
import ru.dbotthepony.kstarbound.util.formatBytesShort import ru.dbotthepony.kstarbound.util.formatBytesShort
@ -159,6 +163,24 @@ class StarboundClient : AutoCloseable {
val settings = ClientSettings() 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 { fun renderFrame(): Boolean {
ensureSameThread() ensureSameThread()
@ -176,10 +198,22 @@ class StarboundClient : AutoCloseable {
val maxs = -mins val maxs = -mins
gl.matrixStack.push() gl.matrixStack.push()
.translateWithScale(viewportWidth / 2f - camera.pos.x, viewportHeight / 2f - camera.pos.y) // центр экрана + координаты отрисовки мира .translateWithScale(viewportWidth / 2f, viewportHeight / 2f, 2f) // центр экрана + координаты отрисовки мира
.scale(x = settings.scale, y = settings.scale) // масштабируем до нужного размера .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() gl.matrixStack.pop()
@ -210,6 +244,10 @@ class StarboundClient : AutoCloseable {
gl.matrixStack.pop() gl.matrixStack.pop()
} }
for (fn in onDrawGUI) {
fn.invoke()
}
val runtime = Runtime.getRuntime() val runtime = Runtime.getRuntime()
gl.font.render("FPS: ${(averageFramesPerSecond * 100f).toInt() / 100f}", scale = 0.4f) gl.font.render("FPS: ${(averageFramesPerSecond * 100f).toInt() / 100f}", scale = 0.4f)

View File

@ -0,0 +1,4 @@
package ru.dbotthepony.kstarbound.client
class UserInput {
}

View File

@ -38,11 +38,15 @@ data class AttributeListPosition(val name: String, val index: Int, val glType: G
class GLFlatAttributeList(builder: GLFlatAttributeListBuilder) : IGLAttributeList { class GLFlatAttributeList(builder: GLFlatAttributeListBuilder) : IGLAttributeList {
val attributes: List<AttributeListPosition> val attributes: List<AttributeListPosition>
val size get() = attributes.size val size get() = attributes.size
/**
* Шаг данных аттрибутов, в байтах. Т.е. одна полная вершина будет занимать [stride] байт в памяти.
*/
val stride: Int val stride: Int
operator fun get(index: Int) = attributes[index] operator fun get(index: Int) = attributes[index]
fun vertexBuilder(vertexType: VertexType) = VertexBuilder(this, vertexType) fun vertexBuilder(vertexType: VertexType) = DynamicVertexBuilder(this, vertexType)
init { init {
val buildList = ArrayList<AttributeListPosition>() val buildList = ArrayList<AttributeListPosition>()
@ -81,6 +85,7 @@ class GLFlatAttributeList(builder: GLFlatAttributeListBuilder) : IGLAttributeLis
} }
companion object { companion object {
val VEC2F = GLFlatAttributeListBuilder().also {it.push(GLType.VEC2F)}.build()
val VEC3F = GLFlatAttributeListBuilder().also {it.push(GLType.VEC3F)}.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_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() val VERTEX_2D_TEXTURE = GLFlatAttributeListBuilder().also {it.push(GLType.VEC2F).push(GLType.VEC2F)}.build()

View File

@ -9,7 +9,6 @@ import ru.dbotthepony.kstarbound.client.freetype.FreeType
import ru.dbotthepony.kstarbound.math.Matrix4f import ru.dbotthepony.kstarbound.math.Matrix4f
import ru.dbotthepony.kstarbound.math.Matrix4fStack import ru.dbotthepony.kstarbound.math.Matrix4fStack
import ru.dbotthepony.kstarbound.client.render.Font 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.client.render.TileRenderers
import ru.dbotthepony.kstarbound.util.Color import ru.dbotthepony.kstarbound.util.Color
import java.io.File import java.io.File
@ -83,6 +82,11 @@ interface GLCleanable : Cleaner.Cleanable {
fun cleanManual(): Unit fun cleanManual(): Unit
} }
interface GLStreamBuilderList {
val small: StreamVertexBuilder
val statefulSmall: StatefulStreamVertexBuilder
}
class GLStateTracker { class GLStateTracker {
init { init {
// This line is critical for LWJGL's interoperation with GLFW's // This line is critical for LWJGL's interoperation with GLFW's
@ -329,11 +333,70 @@ class GLStateTracker {
fragment.unlink() 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 matrixStack = Matrix4fStack()
val freeType = FreeType() val freeType = FreeType()
val font = Font(this) 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 { companion object {
private val LOGGER = LogManager.getLogger(GLStateTracker::class.java) private val LOGGER = LogManager.getLogger(GLStateTracker::class.java)
} }

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.client.gl package ru.dbotthepony.kstarbound.client.gl
import org.lwjgl.opengl.GL46.* import org.lwjgl.opengl.GL46.*
import org.lwjgl.system.MemoryUtil
import java.io.Closeable import java.io.Closeable
import java.nio.ByteBuffer import java.nio.ByteBuffer
@ -41,6 +42,20 @@ class GLVertexBufferObject(val state: GLStateTracker, val type: VBOType = VBOTyp
return this 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 { fun bufferData(data: IntArray, usage: Int): GLVertexBufferObject {
check(isValid) { "Tried to use NULL GLVertexBufferObject" } check(isValid) { "Tried to use NULL GLVertexBufferObject" }
state.ensureSameThread() state.ensureSameThread()

View File

@ -1,16 +1,87 @@
package ru.dbotthepony.kstarbound.client.gl package ru.dbotthepony.kstarbound.client.gl
import org.lwjgl.opengl.GL46.* import org.lwjgl.opengl.GL46.*
import ru.dbotthepony.kstarbound.math.AABB
import java.io.Closeable
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
import kotlin.collections.ArrayList
enum class VertexType(val elements: Int, val indicies: IntArray) { enum class VertexType(val elements: Int, val indicies: IntArray) {
LINES(2, intArrayOf(0, 1)),
TRIANGLES(3, intArrayOf(0, 1, 2)), 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<This : IVertexBuilder<This, VertexType>, VertexType : IVertex<VertexType, This>> {
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<This : IVertex<This, VertexBuilderType>, 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 emptyTransform: VertexTransformer = { it, _ -> it }
private val EMPTY_BUFFER = ByteBuffer.allocateDirect(0)
object VertexTransformers { object VertexTransformers {
fun uv(u0: Float, 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<DynamicVertexBuilder, DynamicVertexBuilder.Vertex> {
private val verticies = ArrayList<Vertex>() private val verticies = ArrayList<Vertex>()
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() verticies.clear()
return this return this
} }
fun vertex(): Vertex { override fun vertex(): Vertex {
return Vertex() return Vertex()
} }
fun quadZ( override fun checkValid() {
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() {
for (vertex in verticies) { for (vertex in verticies) {
vertex.checkValid() 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 * Загружает буфер в указанные 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(vbo.isArray) { "$vbo is not an array" }
require(ebo.isElementArray) { "$vbo is not an element 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})" } 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() checkValid()
ebo.bind()
if (verticies.size == 0) { if (verticies.size == 0) {
vbo.bufferData(intArrayOf(), drawType) vbo.bufferData(EMPTY_BUFFER, drawType)
ebo.bufferData(intArrayOf(), drawType) ebo.bufferData(EMPTY_BUFFER, drawType)
return return
} }
val bytes = ByteBuffer.allocateDirect(verticies.size * attributes.stride) val vertexBuffer = ByteBuffer.allocateDirect(verticies.size * attributes.stride)
bytes.order(ByteOrder.nativeOrder()) vertexBuffer.order(ByteOrder.nativeOrder())
for (vertex in verticies) { val elementBuffer = ByteBuffer.allocateDirect((verticies.size / type.elements) * type.indicies.size * 4)
vertex.upload(bytes) 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) check(vertexBuffer.position() == vertexBuffer.capacity()) { "Vertex Buffer is not fully filled (position: ${vertexBuffer.position()}; capacity: ${vertexBuffer.capacity()})" }
vbo.bufferData(bytes, drawType) 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) vertexBuffer.position(0)
var offset = 0 elementBuffer.position(0)
var offsetVertex = 0
for (i in 0 until verticies.size / type.elements) { vbo.bufferData(vertexBuffer, drawType)
for (i2 in type.indicies.indices) { ebo.bufferData(elementBuffer, drawType)
elementIndicies[offset + i2] = type.indicies[i2] + offsetVertex
}
offset += type.indicies.size
offsetVertex += type.elements
}
ebo.bufferData(elementIndicies, drawType)
} }
inner class Vertex { inner class Vertex : IVertex<Vertex, DynamicVertexBuilder> {
init { init {
verticies.add(this) verticies.add(this)
} }
@ -193,7 +246,7 @@ class VertexBuilder(val attributes: GLFlatAttributeList, private val type: Verte
} }.joinToString("; ")})" } }.joinToString("; ")})"
} }
fun expect(name: String): Vertex { override fun expect(name: String): Vertex {
if (index >= attributes.size) { if (index >= attributes.size) {
throw IllegalStateException("Reached end of attribute list early, expected $name") 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 return this
} }
fun expect(type: GLType): Vertex { override fun expect(type: GLType): Vertex {
if (index >= attributes.size) { if (index >= attributes.size) {
throw IllegalStateException("Reached end of attribute list early, expected type $type") 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 return this
} }
fun pushVec3f(x: Float, y: Float, z: Float): Vertex { override fun pushVec3f(x: Float, y: Float, z: Float): Vertex {
expect(GLType.VEC3F) expect(GLType.VEC3F)
store[index++] = floatArrayOf(x, y, z) store[index++] = floatArrayOf(x, y, z)
return this return this
} }
fun pushVec2f(x: Float, y: Float): Vertex { override fun pushVec2f(x: Float, y: Float): Vertex {
expect(GLType.VEC2F) expect(GLType.VEC2F)
store[index++] = floatArrayOf(x, y) store[index++] = floatArrayOf(x, y)
return this return this
} }
fun checkValid() { override fun checkValid() {
for (elem in store.indices) { for (elem in store.indices) {
if (store[elem] == null) { if (store[elem] == null) {
throw IllegalStateException("Vertex element at position $elem is 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() 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<StreamVertexBuilder, StreamVertexBuilder.Vertex> {
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<Vertex, StreamVertexBuilder> {
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<StreamVertexBuilder, StreamVertexBuilder.Vertex> 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()
}
}

View File

@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound.client.render
import org.lwjgl.opengl.GL46.* import org.lwjgl.opengl.GL46.*
import ru.dbotthepony.kstarbound.client.gl.GLShaderProgram import ru.dbotthepony.kstarbound.client.gl.GLShaderProgram
import ru.dbotthepony.kstarbound.client.gl.GLVertexArrayObject 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.client.gl.checkForGLError
import ru.dbotthepony.kstarbound.math.FloatMatrix import ru.dbotthepony.kstarbound.math.FloatMatrix
@ -45,7 +45,7 @@ class BakedStaticMesh(
) : AutoCloseable { ) : AutoCloseable {
private var onClose = {} private var onClose = {}
constructor(programState: BakedProgramState, builder: VertexBuilder) : this( constructor(programState: BakedProgramState, builder: DynamicVertexBuilder) : this(
programState, programState,
builder.indexCount, builder.indexCount,
programState.program.state.newVAO(), programState.program.state.newVAO(),

View File

@ -7,7 +7,7 @@ class Camera {
/** /**
* Позиция этой камеры в Starbound Unit'ах * Позиция этой камеры в Starbound Unit'ах
*/ */
val pos = MutableVector3f() val pos = MutableVector2f()
var pressedLeft = false var pressedLeft = false
private set private set
@ -30,22 +30,30 @@ class Camera {
} }
} }
fun tick(delta: Double) { val velocity: MutableVector2f get() {
val vec = MutableVector2f()
if (pressedLeft) { if (pressedLeft) {
pos.x -= (delta * FREEVIEW_SENS).toFloat() vec.x -= (FREEVIEW_SENS).toFloat()
} }
if (pressedRight) { if (pressedRight) {
pos.x += (delta * FREEVIEW_SENS).toFloat() vec.x += (FREEVIEW_SENS).toFloat()
} }
if (pressedUp) { if (pressedUp) {
pos.y += (delta * FREEVIEW_SENS).toFloat() vec.y += (FREEVIEW_SENS).toFloat()
} }
if (pressedDown) { 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 { companion object {

View File

@ -7,6 +7,7 @@ import ru.dbotthepony.kstarbound.math.FloatMatrix
import ru.dbotthepony.kstarbound.math.Matrix4f import ru.dbotthepony.kstarbound.math.Matrix4f
import ru.dbotthepony.kstarbound.math.Matrix4fStack import ru.dbotthepony.kstarbound.math.Matrix4fStack
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE 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.Chunk
import ru.dbotthepony.kstarbound.world.ITileChunk import ru.dbotthepony.kstarbound.world.ITileChunk
import kotlin.collections.ArrayList 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) 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<BakedStaticMesh>() private val unloadableBakedMeshes = ArrayList<BakedStaticMesh>()
@ -217,13 +220,25 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Clie
foreground.autoUpload() 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<Pair<BakedStaticMesh, Int>>() private val meshDeque = ArrayDeque<Pair<BakedStaticMesh, Int>>()
override fun renderLayerFromStack(zPos: Int, transform: Matrix4fStack): Int { override fun renderLayerFromStack(zPos: Int, transform: Matrix4fStack): Int {
if (meshDeque.isEmpty()) if (meshDeque.isEmpty())
return -1 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() var pair = meshDeque.last()
while (pair.second >= zPos) { while (pair.second >= zPos) {

View File

@ -301,7 +301,7 @@ class Font(
ebo.bind() ebo.bind()
vbo.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.quad(0f, 0f, width, height, VertexTransformers.uv())
builder.upload(vbo, ebo, GL_STATIC_DRAW) builder.upload(vbo, ebo, GL_STATIC_DRAW)

View File

@ -15,13 +15,13 @@ import kotlin.collections.HashMap
data class TileLayer( data class TileLayer(
val bakedProgramState: BakedProgramState, val bakedProgramState: BakedProgramState,
val vertexBuilder: VertexBuilder, val vertexBuilder: DynamicVertexBuilder,
val zPos: Int) val zPos: Int)
class TileLayerList { class TileLayerList {
private val layers = HashMap<BakedProgramState, ArrayList<TileLayer>>() private val layers = HashMap<BakedProgramState, ArrayList<TileLayer>>()
fun getLayer(programState: BakedProgramState, zLevel: Int, compute: () -> VertexBuilder): VertexBuilder { fun getLayer(programState: BakedProgramState, zLevel: Int, compute: () -> DynamicVertexBuilder): DynamicVertexBuilder {
val list = layers.computeIfAbsent(programState) {ArrayList()} val list = layers.computeIfAbsent(programState) {ArrayList()}
for (layer in list) { for (layer in list) {
@ -160,25 +160,25 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) {
val bakedBackgroundProgramState = state.tileRenderers.background(texture) val bakedBackgroundProgramState = state.tileRenderers.background(texture)
// private var notifiedDepth = false // 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 fx = pos.x.toFloat()
val fy = pos.y.toFloat() val fy = pos.y.toFloat()
var a = fx var a = fx
var b = fy var b = fy
var c = fx + piece.textureSize.x / BASELINE_TEXTURE_SIZE var c = fx + piece.textureSize.x / PIXELS_IN_STARBOUND_UNITf
var d = fy + piece.textureSize.y / BASELINE_TEXTURE_SIZE var d = fy + piece.textureSize.y / PIXELS_IN_STARBOUND_UNITf
if (offset != Vector2i.ZERO) { if (offset != Vector2i.ZERO) {
a += offset.x / BASELINE_TEXTURE_SIZE a += offset.x / PIXELS_IN_STARBOUND_UNITf
// в json файлах y указан как положительный вверх, // в json файлах y указан как положительный вверх,
// что соответствует нашему миру // что соответствует нашему миру
b += offset.y / BASELINE_TEXTURE_SIZE b += offset.y / PIXELS_IN_STARBOUND_UNITf
c += offset.x / BASELINE_TEXTURE_SIZE c += offset.x / PIXELS_IN_STARBOUND_UNITf
d += offset.y / BASELINE_TEXTURE_SIZE d += offset.y / PIXELS_IN_STARBOUND_UNITf
} }
if (tile.render.variants == 0 || piece.texture != null || piece.variantStride == null) { 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) val (u1, v1) = texture.pixelToUV(piece.texturePosition + piece.textureSize)
builder.quadZ( builder.quadZ(
a * PIXELS_IN_STARBOUND_UNITf, a,
b * PIXELS_IN_STARBOUND_UNITf, b,
c * PIXELS_IN_STARBOUND_UNITf, c,
d * PIXELS_IN_STARBOUND_UNITf, d,
Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0)) Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0))
} else { } else {
val variant = (getter.randomDoubleFor(pos) * tile.render.variants).toInt() 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) val (u1, v1) = texture.pixelToUV(piece.texturePosition + piece.textureSize + piece.variantStride * variant)
builder.quadZ( builder.quadZ(
a * PIXELS_IN_STARBOUND_UNITf, a,
b * PIXELS_IN_STARBOUND_UNITf, b,
c * PIXELS_IN_STARBOUND_UNITf, c,
d * PIXELS_IN_STARBOUND_UNITf, d,
Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0)) 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)) { if (matchPiece.test(getter, tile, pos)) {
for (renderPiece in matchPiece.pieces) { for (renderPiece in matchPiece.pieces) {
if (renderPiece.piece.texture != null) { 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) { 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) }, pos, renderPiece.offset)
} else { } else {
tesselateAt(renderPiece.piece, getter, thisBuilder, pos, renderPiece.offset) tesselateAt(renderPiece.piece, getter, thisBuilder, pos, renderPiece.offset)
@ -259,7 +259,7 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) {
tile.render.renderTemplate ?: return tile.render.renderTemplate ?: return
val builder = layers.getLayer(if (background) bakedBackgroundProgramState else bakedProgramState, tile.render.zLevel) { 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) { for ((_, matcher) in tile.render.renderTemplate.matches) {
@ -274,7 +274,6 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) {
} }
companion object { companion object {
const val BASELINE_TEXTURE_SIZE = 8f
const val Z_LEVEL = 10f const val Z_LEVEL = 10f
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
} }

View File

@ -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<T>(private val factory: (x: Int, y: Int) -> T) : kotlin.collections.Iterator<T> {
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<Vector2i> get() = Iterator(::Vector2i)
val chunkPositions: kotlin.collections.Iterator<ChunkPos> get() = Iterator(::ChunkPos)
}

View File

@ -21,6 +21,10 @@ interface IMatrixLikeFloat : IMatrixLike {
operator fun get(row: Int, column: Int): Float operator fun get(row: Int, column: Int): Float
} }
interface IMatrixLikeDouble : IMatrixLike {
operator fun get(row: Int, column: Int): Double
}
interface IMatrix : IMatrixLike { interface IMatrix : IMatrixLike {
operator fun plus(other: IMatrix): IMatrix operator fun plus(other: IMatrix): IMatrix
operator fun minus(other: IMatrix): IMatrix operator fun minus(other: IMatrix): IMatrix

View File

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

View File

@ -1,12 +1,17 @@
package ru.dbotthepony.kstarbound.math package ru.dbotthepony.kstarbound.math
import com.google.gson.JsonArray import com.google.gson.JsonArray
import ru.dbotthepony.kstarbound.api.IStruct2f import ru.dbotthepony.kstarbound.api.*
import ru.dbotthepony.kstarbound.api.IStruct2i
import ru.dbotthepony.kstarbound.api.IStruct3f
import ru.dbotthepony.kstarbound.api.IStruct4f
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sin import kotlin.math.sin
import kotlin.math.sqrt
// Так как у нас нет шаблонов ни в Java, ни в Kotlin
// а дженерики вызывают autoboxing
// приходится создавать "бетонные" реализации для каждого вида вектора
// а ведь компилятор мог бы это генерировать.
abstract class IVector2i<T : IVector2i<T>> : IMatrixLike, IMatrixLikeInt, IStruct2i { abstract class IVector2i<T : IVector2i<T>> : IMatrixLike, IMatrixLikeInt, IStruct2i {
override val columns = 1 override val columns = 1
@ -20,13 +25,76 @@ abstract class IVector2i<T : IVector2i<T>> : IMatrixLike, IMatrixLikeInt, IStruc
operator fun times(other: IVector2i<*>) = make(x * other.x, y * other.y) 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 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 div(other: Int) = make(x / other, y / other)
operator fun times(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 minus(other: Int) = make(x - other, y - other)
operator fun plus(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) 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 left() = make(x - 1, y)
fun right() = make(x + 1, y) fun right() = make(x + 1, y)
fun up() = make(x, y + 1) fun up() = make(x, y + 1)
@ -45,6 +113,9 @@ abstract class IVector2i<T : IVector2i<T>> : IMatrixLike, IMatrixLikeInt, IStruc
} }
protected abstract fun make(x: Int, y: Int): T 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<Vector2i>() { data class Vector2i(override val x: Int = 0, override val y: Int = 0) : IVector2i<Vector2i>() {
@ -101,6 +172,49 @@ abstract class IVector2f<T : IVector2f<T>> : IMatrixLike, IMatrixLikeFloat, IStr
fun up() = make(x, y + 1) fun up() = make(x, y + 1)
fun down() = 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 { override fun get(row: Int, column: Int): Float {
if (column != 0) { if (column != 0) {
throw IndexOutOfBoundsException("Column must be 0 ($column given)") throw IndexOutOfBoundsException("Column must be 0 ($column given)")
@ -114,6 +228,8 @@ abstract class IVector2f<T : IVector2f<T>> : IMatrixLike, IMatrixLikeFloat, IStr
} }
protected abstract fun make(x: Float, y: Float): T 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<Vector2f>() { data class Vector2f(override val x: Float = 0f, override val y: Float = 0f) : IVector2f<Vector2f>() {
@ -146,6 +262,122 @@ data class MutableVector2f(override var x: Float = 0f, override var y: Float = 0
} }
} }
abstract class IVector2d<T : IVector2d<T>> : 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<Vector2d>() {
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<MutableVector2d>() {
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<T : IVector3f<T>> : IMatrixLike, IMatrixLikeFloat, IStruct3f { abstract class IVector3f<T : IVector3f<T>> : IMatrixLike, IMatrixLikeFloat, IStruct3f {
override val columns = 1 override val columns = 1
override val rows = 3 override val rows = 3
@ -166,6 +398,12 @@ abstract class IVector3f<T : IVector3f<T>> : IMatrixLike, IMatrixLikeFloat, IStr
operator fun unaryMinus() = make(-x, -y, -z) 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 { override fun get(row: Int, column: Int): Float {
if (column != 0) { if (column != 0) {
throw IndexOutOfBoundsException("Column must be 0 ($column given)") 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 return this
} }
} }
abstract class IVector4d<T : IVector4d<T>> : 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<Vector4d>() {
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<MutableVector4d>() {
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
}
}

View File

@ -9,8 +9,8 @@ private const val PETIBYTE = TEBIBYTE * 1024L
fun formatBytesShort(input: Long): String { fun formatBytesShort(input: Long): String {
return when (input) { return when (input) {
in 0 until KIBIBYTE -> "${input}b" in 0 until KIBIBYTE -> "${input}b"
in KIBIBYTE until MEBIBYTE -> "%.2fKiB".format((input / KIBIBYTE).toDouble() + (input % KIBIBYTE).toDouble() / KIBIBYTE) in KIBIBYTE until MEBIBYTE -> "${(((input / KIBIBYTE).toDouble() + (input % KIBIBYTE).toDouble() / KIBIBYTE) * 100.0).toLong().toDouble() / 100.0}KiB"
in MEBIBYTE until GIBIBYTE -> "%.2fMiB".format((input / MEBIBYTE).toDouble() + (input % MEBIBYTE).toDouble() / MEBIBYTE) in MEBIBYTE until GIBIBYTE -> "${(((input / MEBIBYTE).toDouble() + (input % MEBIBYTE).toDouble() / MEBIBYTE) * 100.0).toLong().toDouble() / 100.0}MiB"
else -> "${input}b" else -> "${input}b"
} }
} }

View File

@ -2,8 +2,12 @@ package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.api.IStruct2i import ru.dbotthepony.kstarbound.api.IStruct2i
import ru.dbotthepony.kstarbound.defs.TileDefinition import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.IVector2i import ru.dbotthepony.kstarbound.math.IVector2i
import ru.dbotthepony.kstarbound.math.Vector2d
import ru.dbotthepony.kstarbound.math.Vector2i 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_SHIFT = 5
const val CHUNK_SIZE = 1 shl CHUNK_SHIFT // 32 const val CHUNK_SIZE = 1 shl CHUNK_SHIFT // 32
const val CHUNK_SIZE_FF = CHUNK_SIZE - 1 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<ChunkPos>() { data class ChunkPos(override val x: Int, override val y: Int) : IVector2i<ChunkPos>() {
constructor(pos: IStruct2i) : this(pos.component1(), pos.component2()) constructor(pos: IStruct2i) : this(pos.component1(), pos.component2())
@ -158,7 +165,25 @@ data class ChunkPos(override val x: Int, override val y: Int) : IVector2i<ChunkP
companion object { companion object {
fun fromTilePosition(input: IStruct2i): ChunkPos { fun fromTilePosition(input: IStruct2i): ChunkPos {
val (x, y) = input val (x, y) = input
return ChunkPos(x shr CHUNK_SHIFT, y shr CHUNK_SHIFT) return ChunkPos(tileToChunkComponent(x), tileToChunkComponent(y))
}
fun fromTilePosition(x: Int, y: Int): ChunkPos {
return ChunkPos(tileToChunkComponent(x), tileToChunkComponent(y))
}
fun normalizeCoordinate(input: Int): Int {
val band = input and CHUNK_SIZE_FF
if (band < 0) {
return band + CHUNK_SIZE_FF
}
return band
}
fun tileToChunkComponent(comp: Int): Int {
return comp shr CHUNK_SHIFT
} }
} }
} }
@ -294,6 +319,8 @@ open class Chunk(val world: World<*>?, val pos: ChunkPos) {
changeset++ changeset++
} }
val aabb = aabbBase + Vector2d(pos.x * CHUNK_SIZE.toDouble(), pos.y * CHUNK_SIZE.toDouble())
inner class TileLayer : IMutableTileChunk { inner class TileLayer : IMutableTileChunk {
/** /**
* Возвращает счётчик изменений этого слоя * Возвращает счётчик изменений этого слоя
@ -306,6 +333,65 @@ open class Chunk(val world: World<*>?, val pos: ChunkPos) {
this@Chunk.changeset++ this@Chunk.changeset++
} }
private val collisionCache = ArrayList<AABB>()
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<AABB> {
if (collisionChangeset != changeset) {
bakeCollisions()
}
return collisionCacheView
}
override val pos: ChunkPos override val pos: ChunkPos
get() = this@Chunk.pos 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 get(x: Int, y: Int): ChunkTile? = null
override fun set(x: Int, y: Int, tile: TileDefinition?): 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()),
)
} }
} }

View File

@ -1,6 +1,9 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.defs.TileDefinition import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kstarbound.math.Vector2d
import ru.dbotthepony.kstarbound.math.Vector2i import ru.dbotthepony.kstarbound.math.Vector2i
/** /**
@ -40,11 +43,19 @@ open class MutableWorldChunkTuple(
override var bottom: IWorldChunkTuple?, override var bottom: IWorldChunkTuple?,
) : IMutableWorldChunkTuple ) : IMutableWorldChunkTuple
@Suppress("WeakerAccess") const val EARTH_FREEFALL_ACCELERATION = 9.8312 / METRES_IN_STARBOUND_UNIT
abstract class World<T : IMutableWorldChunkTuple>(val seed: Long = 0L) { abstract class World<T : IMutableWorldChunkTuple>(val seed: Long = 0L) {
protected val chunkMap = HashMap<ChunkPos, T>() protected val chunkMap = HashMap<ChunkPos, T>()
protected var lastAccessedChunk: T? = null protected var lastAccessedChunk: T? = null
/**
* Стандартное ускорение свободного падения в Starbound Units/секунда^2
*
* При Vector2d.ZERO = невесомость
*/
var gravity = Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION)
protected abstract fun tupleFactory( protected abstract fun tupleFactory(
chunk: Chunk, chunk: Chunk,
top: IWorldChunkTuple?, top: IWorldChunkTuple?,
@ -155,22 +166,56 @@ abstract class World<T : IMutableWorldChunkTuple>(val seed: Long = 0L) {
} }
fun getTile(pos: Vector2i): ChunkTile? { 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 { fun setTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple {
val chunk = computeIfAbsentInternal(ChunkPos.fromTilePosition(pos)) 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 return chunk
} }
fun getBackgroundTile(pos: Vector2i): ChunkTile? { 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 { fun setBackgroundTile(pos: Vector2i, tile: TileDefinition?): IWorldChunkTuple {
val chunk = computeIfAbsentInternal(ChunkPos.fromTilePosition(pos)) 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 return chunk
} }
protected open fun collectInternal(boundingBox: AABBi): List<T> {
val output = ArrayList<T>()
for (pos in boundingBox.chunkPositions) {
val chunk = getChunkInternal(pos)
if (chunk != null) {
output.add(chunk)
}
}
return output
}
/**
* Возвращает все чанки, которые пересекаются с заданным [boundingBox]
*/
open fun collect(boundingBox: AABBi): List<IWorldChunkTuple> {
val output = ArrayList<IWorldChunkTuple>()
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
}
} }

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
#version 460
uniform vec4 _color;
out vec4 _color_out;
void main() {
_color_out = _color;
}

View File

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

View File

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