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

обрезка геометрии мира
тест коллизий
сущности
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.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
}
//}
}
}

View File

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

View File

@ -6,7 +6,7 @@ data class ClientSettings(
*
* Масштаб в единицу означает что один 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
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<ClientWo
}
/**
* Отрисовывает этот мир с точки зрения [pos] в Starbound Units
* Отрисовывает этот с обрезкой невидимой геометрии с точки зрения [size] в Starbound Units
*
* Все координаты "местности" сохраняются, поэтому, если отрисовывать слишком далеко от 0, 0
* то геометрия может начать искажаться из-за погрешности плавающей запятой
@ -55,25 +55,22 @@ class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWo
* Обрезает всю заведомо невидимую геометрию на основе аргументов mins и maxs (в пикселях)
*/
fun render(
pos: IStruct2f,
scale: Float = 1f,
mins: IStruct2f,
maxs: IStruct2f,
size: AABB,
) {
val determineRenderers = ArrayList<ChunkRenderer>()
for (chunk in chunkMap.values) {
for (chunk in collectInternal(size.encasingChunkPosAABB())) {
determineRenderers.add(chunk.renderer)
}
val renderList = ArrayList<ChunkRenderer>()
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()
}
}
}

View File

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

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 {
val attributes: List<AttributeListPosition>
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<AttributeListPosition>()
@ -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()

View File

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

View File

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

View File

@ -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<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 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<DynamicVertexBuilder, DynamicVertexBuilder.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()
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<Vertex, DynamicVertexBuilder> {
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<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 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(),

View File

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

View File

@ -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<BakedStaticMesh>()
@ -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<Pair<BakedStaticMesh, Int>>()
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) {

View File

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

View File

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

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

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
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<T : IVector2i<T>> : IMatrixLike, IMatrixLikeInt, IStruct2i {
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 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<T : IVector2i<T>> : 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<Vector2i>() {
@ -101,6 +172,49 @@ abstract class IVector2f<T : IVector2f<T>> : 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<T : IVector2f<T>> : 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<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 {
override val columns = 1
override val rows = 3
@ -166,6 +398,12 @@ abstract class IVector3f<T : IVector3f<T>> : 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<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 {
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"
}
}

View File

@ -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<ChunkPos>() {
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 {
fun fromTilePosition(input: IStruct2i): ChunkPos {
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++
}
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<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
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()),
)
}
}

View File

@ -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<T : IMutableWorldChunkTuple>(val seed: Long = 0L) {
protected val chunkMap = HashMap<ChunkPos, T>()
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<T : IMutableWorldChunkTuple>(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<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() }
}
}