diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
index a7979d00..076e0d3f 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt
@@ -2,22 +2,13 @@ package ru.dbotthepony.kstarbound
 
 import org.apache.logging.log4j.LogManager
 import org.lwjgl.Version
-import org.lwjgl.glfw.GLFW.*
-import org.lwjgl.opengl.GL46.*
 import ru.dbotthepony.kstarbound.client.StarboundClient
-import ru.dbotthepony.kstarbound.math.Matrix4f
-import ru.dbotthepony.kstarbound.client.render.Camera
-import ru.dbotthepony.kstarbound.client.render.ChunkRenderer
-import ru.dbotthepony.kstarbound.client.render.TextAlignX
-import ru.dbotthepony.kstarbound.client.render.TextAlignY
 import ru.dbotthepony.kstarbound.defs.TileDefinition
-import ru.dbotthepony.kstarbound.util.Color
-import ru.dbotthepony.kstarbound.util.formatBytesShort
-import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
+import ru.dbotthepony.kstarbound.math.Vector2d
 import ru.dbotthepony.kstarbound.world.Chunk
 import ru.dbotthepony.kstarbound.world.ChunkPos
+import ru.dbotthepony.kstarbound.world.entities.Humanoid
 import java.io.File
-import java.util.*
 
 private val LOGGER = LogManager.getLogger()
 
@@ -41,43 +32,38 @@ fun main() {
 	Starbound.onInitialize {
 		chunkA = client.world!!.computeIfAbsent(ChunkPos(0, 0)).chunk
 		val chunkB = client.world!!.computeIfAbsent(ChunkPos(-1, 0)).chunk
-
-		var x = 0
-		var y = 0
-
-		for (tile in Starbound.tilesAccess.values) {
-			//chunkA!!.background[x, y + 1] = tile
-			//chunkA!!.background[x++, y] = tile
-
-			if (x >= 31) {
-				x = 0
-				y += 2
-			}
-		}
-
-		x = 0
-		y = 0
-
-		for (tile in Starbound.tilesAccess.values) {
-			//chunkB.foreground[x, y + 1] = tile
-			//chunkB.foreground[x++, y] = tile
-
-			if (x > 31) {
-				x = 0
-				y += 2
-			}
-		}
+		val chunkC = client.world!!.computeIfAbsent(ChunkPos(-2, 0)).chunk
 
 		val tile = Starbound.getTileDefinition("alienrock")
 
-		for (x in 0 .. 31) {
-			for (y in 0 .. 31) {
-				chunkA!!.foreground[x, y] = tile
+		for (x in -48 .. 48) {
+			for (y in 0 .. 20) {
+				val chnk = client.world!!.computeIfAbsent(ChunkPos(x, y))
+
+				for (bx in 0 .. 31) {
+					for (by in 0 .. 3) {
+						chnk.chunk.foreground[bx, by] = tile
+					}
+				}
 			}
 		}
 
 		for (x in 0 .. 31) {
-			for (y in 0 .. 31) {
+			for (y in 0 .. 3) {
+				chunkA!!.foreground[x, y] = tile
+				chunkC.foreground[x, y] = tile
+			}
+		}
+
+		for (x in 0 .. 31) {
+			for (y in 8 .. 9) {
+				chunkA!!.foreground[x, y] = tile
+				chunkC.foreground[x, y] = tile
+			}
+		}
+
+		for (x in 0 .. 31) {
+			for (y in 0 .. 0) {
 				chunkB.foreground[x, y] = tile
 			}
 		}
@@ -87,16 +73,44 @@ fun main() {
 				chunkA!!.foreground[x, y] = null as TileDefinition?
 			}
 		}
+
+		/*val rand = Random()
+
+		for (i in 0 .. 400) {
+			chunkA!!.foreground[rand.nextInt(0, CHUNK_SIZE_FF), rand.nextInt(0, CHUNK_SIZE_FF)] = tile
+		}*/
 	}
 
-	val rand = Random()
+	//val rand = Random()
+	val ent = Humanoid(client.world!!)
+
+	ent.pos += Vector2d(y = 36.0, x = 10.0)
+
+	client.onDrawGUI {
+		client.gl.font.render("${ent.pos}", y = 100f, scale = 0.25f)
+	}
+
+	client.onPostDrawWorld {
+		client.gl.quadWireframe {
+			it.quad(ent.aabb + ent.pos)
+		}
+	}
 
 	while (client.renderFrame()) {
 		Starbound.pollCallbacks()
 
-		if (chunkA != null && glfwGetTime() < 10.0) {
-			val tile = Starbound.getTileDefinition("alienrock")
+		ent.moveAndCollide(client.frameRenderTime)
+		client.camera.pos.x = ent.pos.x.toFloat()
+		client.camera.pos.y = ent.pos.y.toFloat()
+
+		//println(client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1)
+
+		//if (ent.onGround)
+			ent.velocity += client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1
+
+		//if (chunkA != null && glfwGetTime() < 10.0) {
+		//	val tile = Starbound.getTileDefinition("alienrock")
 			//chunkA!!.foreground[rand.nextInt(0, CHUNK_SIZE_FF), rand.nextInt(0, CHUNK_SIZE_FF)] = tile
-		}
+		//}
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
index 9723c35b..12d297b4 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
@@ -9,8 +9,8 @@ import ru.dbotthepony.kstarbound.world.World
 import java.io.File
 import java.io.FileNotFoundException
 
-const val METRES_IN_STARBOUND_UNIT = 0.5
-const val METRES_IN_STARBOUND_UNITf = 0.5f
+const val METRES_IN_STARBOUND_UNIT = 0.25
+const val METRES_IN_STARBOUND_UNITf = 0.25f
 
 const val PIXELS_IN_STARBOUND_UNIT = 8.0
 const val PIXELS_IN_STARBOUND_UNITf = 8.0f
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientSettings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientSettings.kt
index b34ca519..c19323a6 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientSettings.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientSettings.kt
@@ -6,7 +6,7 @@ data class ClientSettings(
 	 *
 	 * Масштаб в единицу означает что один Starbound Unit будет равен 8 пикселям на экране
 	 */
-	var scale: Float = 2f
-) {
+	var scale: Float = 2f,
 
-}
+	var debugCollisions: Boolean = true,
+)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt
index 52f09aab..b2d508bb 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt
@@ -1,12 +1,12 @@
 package ru.dbotthepony.kstarbound.client
 
+import ru.dbotthepony.kstarbound.api.IStruct2d
 import ru.dbotthepony.kstarbound.api.IStruct2f
 import ru.dbotthepony.kstarbound.client.render.ChunkRenderer
 import ru.dbotthepony.kstarbound.client.render.renderLayeredList
-import ru.dbotthepony.kstarbound.world.Chunk
-import ru.dbotthepony.kstarbound.world.IWorldChunkTuple
-import ru.dbotthepony.kstarbound.world.MutableWorldChunkTuple
-import ru.dbotthepony.kstarbound.world.World
+import ru.dbotthepony.kstarbound.math.AABB
+import ru.dbotthepony.kstarbound.math.Vector2d
+import ru.dbotthepony.kstarbound.world.*
 
 class ClientWorldChunkTuple(
 	world: World<*>,
@@ -47,7 +47,7 @@ class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<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()
+		}
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt
index a44b645f..148a7c34 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt
@@ -7,11 +7,15 @@ import org.lwjgl.glfw.GLFWErrorCallback
 import org.lwjgl.opengl.GL46.*
 import org.lwjgl.system.MemoryStack
 import org.lwjgl.system.MemoryUtil
+import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNIT
+import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
 import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
 import ru.dbotthepony.kstarbound.math.Matrix4f
 import ru.dbotthepony.kstarbound.client.render.Camera
 import ru.dbotthepony.kstarbound.client.render.TextAlignX
 import ru.dbotthepony.kstarbound.client.render.TextAlignY
+import ru.dbotthepony.kstarbound.math.AABB
+import ru.dbotthepony.kstarbound.math.Vector2d
 import ru.dbotthepony.kstarbound.math.Vector2f
 import ru.dbotthepony.kstarbound.util.Color
 import ru.dbotthepony.kstarbound.util.formatBytesShort
@@ -159,6 +163,24 @@ class StarboundClient : AutoCloseable {
 
 	val settings = ClientSettings()
 
+	private val onDrawGUI = ArrayList<() -> Unit>()
+
+	fun onDrawGUI(lambda: () -> Unit) {
+		onDrawGUI.add(lambda)
+	}
+
+	private val onPreDrawWorld = ArrayList<() -> Unit>()
+
+	fun onPreDrawWorld(lambda: () -> Unit) {
+		onPreDrawWorld.add(lambda)
+	}
+
+	private val onPostDrawWorld = ArrayList<() -> Unit>()
+
+	fun onPostDrawWorld(lambda: () -> Unit) {
+		onPostDrawWorld.add(lambda)
+	}
+
 	fun renderFrame(): Boolean {
 		ensureSameThread()
 
@@ -176,10 +198,22 @@ class StarboundClient : AutoCloseable {
 		val maxs = -mins
 
 		gl.matrixStack.push()
-			.translateWithScale(viewportWidth / 2f - camera.pos.x, viewportHeight / 2f - camera.pos.y) // центр экрана + координаты отрисовки мира
-			.scale(x = settings.scale, y = settings.scale) // масштабируем до нужного размера
+			.translateWithScale(viewportWidth / 2f, viewportHeight / 2f, 2f) // центр экрана + координаты отрисовки мира
+			.scale(x = settings.scale * PIXELS_IN_STARBOUND_UNITf, y = settings.scale * PIXELS_IN_STARBOUND_UNITf) // масштабируем до нужного размера
+			.translateWithScale(-camera.pos.x, -camera.pos.y) // перемещаем вид к камере
 
-		world?.render(Vector2f.ZERO, mins = mins, maxs = maxs)
+		for (lambda in onPreDrawWorld) {
+			lambda.invoke()
+		}
+
+		world?.render(AABB.rectangle(
+			camera.pos.toDoubleVector(),
+			viewportWidth / settings.scale / PIXELS_IN_STARBOUND_UNIT,
+			viewportHeight / settings.scale / PIXELS_IN_STARBOUND_UNIT))
+
+		for (lambda in onPostDrawWorld) {
+			lambda.invoke()
+		}
 
 		gl.matrixStack.pop()
 
@@ -210,6 +244,10 @@ class StarboundClient : AutoCloseable {
 			gl.matrixStack.pop()
 		}
 
+		for (fn in onDrawGUI) {
+			fn.invoke()
+		}
+
 		val runtime = Runtime.getRuntime()
 
 		gl.font.render("FPS: ${(averageFramesPerSecond * 100f).toInt() / 100f}", scale = 0.4f)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/UserInput.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/UserInput.kt
new file mode 100644
index 00000000..18a98c9c
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/UserInput.kt
@@ -0,0 +1,4 @@
+package ru.dbotthepony.kstarbound.client
+
+class UserInput {
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLAttributeList.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLAttributeList.kt
index 0ba7c84c..974c246a 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLAttributeList.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLAttributeList.kt
@@ -38,11 +38,15 @@ data class AttributeListPosition(val name: String, val index: Int, val glType: G
 class GLFlatAttributeList(builder: GLFlatAttributeListBuilder) : IGLAttributeList {
 	val attributes: List<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()
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt
index e3d7b584..9414dd0b 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt
@@ -9,7 +9,6 @@ import ru.dbotthepony.kstarbound.client.freetype.FreeType
 import ru.dbotthepony.kstarbound.math.Matrix4f
 import ru.dbotthepony.kstarbound.math.Matrix4fStack
 import ru.dbotthepony.kstarbound.client.render.Font
-import ru.dbotthepony.kstarbound.client.render.TileRenderer
 import ru.dbotthepony.kstarbound.client.render.TileRenderers
 import ru.dbotthepony.kstarbound.util.Color
 import java.io.File
@@ -83,6 +82,11 @@ interface GLCleanable : Cleaner.Cleanable {
 	fun cleanManual(): Unit
 }
 
+interface GLStreamBuilderList {
+	val small: StreamVertexBuilder
+	val statefulSmall: StatefulStreamVertexBuilder
+}
+
 class GLStateTracker {
 	init {
 		// This line is critical for LWJGL's interoperation with GLFW's
@@ -329,11 +333,70 @@ class GLStateTracker {
 		fragment.unlink()
 	}
 
+	val flatProgram: GLTransformableColorableProgram
+
+	init {
+		val vertex = GLShader.internalVertex("shaders/vertex/flat_vertex_2d.glsl")
+		val fragment = GLShader.internalFragment("shaders/fragment/flat_color.glsl")
+
+		flatProgram = GLTransformableColorableProgram(this, vertex, fragment)
+
+		vertex.unlink()
+		fragment.unlink()
+	}
+
+	val flat2DQuads = object : GLStreamBuilderList {
+		override val small by lazy {
+			return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.QUADS, 1024)
+		}
+
+		override val statefulSmall by lazy {
+			return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small)
+		}
+	}
+
+	val flat2DQuadLines = object : GLStreamBuilderList {
+		override val small by lazy {
+			return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.QUADS_AS_LINES, 1024)
+		}
+
+		override val statefulSmall by lazy {
+			return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small)
+		}
+	}
+
+	val flat2DQuadWireframe = object : GLStreamBuilderList {
+		override val small by lazy {
+			return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.QUADS_AS_LINES_WIREFRAME, 1024)
+		}
+
+		override val statefulSmall by lazy {
+			return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small)
+		}
+	}
+
 	val matrixStack = Matrix4fStack()
 	val freeType = FreeType()
 
 	val font = Font(this)
 
+	fun quadWireframe(lambda: (StreamVertexBuilder) -> Unit) {
+		val stateful = flat2DQuadWireframe.statefulSmall
+		val builder = stateful.builder
+
+		builder.begin()
+
+		lambda.invoke(builder)
+
+		stateful.upload()
+
+		flatProgram.use()
+		flatProgram.color.set(Color.WHITE)
+		flatProgram.transform.set(matrixStack.last)
+
+		stateful.draw(GL_LINES)
+	}
+
 	companion object {
 		private val LOGGER = LogManager.getLogger(GLStateTracker::class.java)
 	}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLVertexBufferObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLVertexBufferObject.kt
index 4043ddb5..ac62cc1d 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLVertexBufferObject.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLVertexBufferObject.kt
@@ -1,6 +1,7 @@
 package ru.dbotthepony.kstarbound.client.gl
 
 import org.lwjgl.opengl.GL46.*
+import org.lwjgl.system.MemoryUtil
 import java.io.Closeable
 import java.nio.ByteBuffer
 
@@ -41,6 +42,20 @@ class GLVertexBufferObject(val state: GLStateTracker, val type: VBOType = VBOTyp
 		return this
 	}
 
+	fun bufferData(data: ByteBuffer, usage: Int, length: Long): GLVertexBufferObject {
+		check(isValid) { "Tried to use NULL GLVertexBufferObject" }
+		state.ensureSameThread()
+
+		if (length > data.remaining().toLong()) {
+			throw IndexOutOfBoundsException("Tried to upload $data into $pointer with offset at ${data.position()} with length of $length, but that is longer than remaining data length of ${data.remaining()}!")
+		}
+
+		nglNamedBufferData(pointer, length, MemoryUtil.memAddress(data), usage)
+
+		checkForGLError()
+		return this
+	}
+
 	fun bufferData(data: IntArray, usage: Int): GLVertexBufferObject {
 		check(isValid) { "Tried to use NULL GLVertexBufferObject" }
 		state.ensureSameThread()
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt
index 50df99b1..246d6f1d 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt
@@ -1,16 +1,87 @@
 package ru.dbotthepony.kstarbound.client.gl
 
 import org.lwjgl.opengl.GL46.*
+import ru.dbotthepony.kstarbound.math.AABB
+import java.io.Closeable
 import java.nio.ByteBuffer
 import java.nio.ByteOrder
+import kotlin.collections.ArrayList
 
 enum class VertexType(val elements: Int, val indicies: IntArray) {
+	LINES(2, intArrayOf(0, 1)),
 	TRIANGLES(3, intArrayOf(0, 1, 2)),
-	QUADS(4, intArrayOf(0, 1, 2, 1, 2, 3))
+	QUADS(4, intArrayOf(0, 1, 2, 1, 2, 3)),
+	QUADS_AS_LINES(4, intArrayOf(0, 1, 0, 2, 1, 3, 2, 3)),
+	QUADS_AS_LINES_WIREFRAME(4, intArrayOf(0, 1, 0, 2, 1, 3, 2, 3, 0, 3, 1, 2)),
 }
 
-typealias VertexTransformer = (VertexBuilder.Vertex, Int) -> VertexBuilder.Vertex
+interface IVertexBuilder<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()
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BakedProgramState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BakedProgramState.kt
index cec83cac..ba343b9d 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BakedProgramState.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/BakedProgramState.kt
@@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound.client.render
 import org.lwjgl.opengl.GL46.*
 import ru.dbotthepony.kstarbound.client.gl.GLShaderProgram
 import ru.dbotthepony.kstarbound.client.gl.GLVertexArrayObject
-import ru.dbotthepony.kstarbound.client.gl.VertexBuilder
+import ru.dbotthepony.kstarbound.client.gl.DynamicVertexBuilder
 import ru.dbotthepony.kstarbound.client.gl.checkForGLError
 import ru.dbotthepony.kstarbound.math.FloatMatrix
 
@@ -45,7 +45,7 @@ class BakedStaticMesh(
 ) : AutoCloseable {
 	private var onClose = {}
 
-	constructor(programState: BakedProgramState, builder: VertexBuilder) : this(
+	constructor(programState: BakedProgramState, builder: DynamicVertexBuilder) : this(
 		programState,
 		builder.indexCount,
 		programState.program.state.newVAO(),
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Camera.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Camera.kt
index 0883585f..be4892e0 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Camera.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Camera.kt
@@ -7,7 +7,7 @@ class Camera {
 	/**
 	 * Позиция этой камеры в Starbound Unit'ах
 	 */
-	val pos = MutableVector3f()
+	val pos = MutableVector2f()
 
 	var pressedLeft = false
 		private set
@@ -30,22 +30,30 @@ class Camera {
 		}
 	}
 
-	fun tick(delta: Double) {
+	val velocity: MutableVector2f get() {
+		val vec = MutableVector2f()
+
 		if (pressedLeft) {
-			pos.x -= (delta * FREEVIEW_SENS).toFloat()
+			vec.x -= (FREEVIEW_SENS).toFloat()
 		}
 
 		if (pressedRight) {
-			pos.x += (delta * FREEVIEW_SENS).toFloat()
+			vec.x += (FREEVIEW_SENS).toFloat()
 		}
 
 		if (pressedUp) {
-			pos.y += (delta * FREEVIEW_SENS).toFloat()
+			vec.y += (FREEVIEW_SENS).toFloat()
 		}
 
 		if (pressedDown) {
-			pos.y -= (delta * FREEVIEW_SENS).toFloat()
+			vec.y -= (FREEVIEW_SENS).toFloat()
 		}
+
+		return vec
+	}
+
+	fun tick(delta: Double) {
+		pos + velocity * delta.toFloat()
 	}
 
 	companion object {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt
index 1eee4d55..a4c8b6c2 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/ChunkRenderer.kt
@@ -7,6 +7,7 @@ import ru.dbotthepony.kstarbound.math.FloatMatrix
 import ru.dbotthepony.kstarbound.math.Matrix4f
 import ru.dbotthepony.kstarbound.math.Matrix4fStack
 import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
+import ru.dbotthepony.kstarbound.world.CHUNK_SIZEf
 import ru.dbotthepony.kstarbound.world.Chunk
 import ru.dbotthepony.kstarbound.world.ITileChunk
 import kotlin.collections.ArrayList
@@ -117,6 +118,8 @@ class ChunkRenderer(val state: GLStateTracker, val chunk: Chunk, val world: Clie
 		}
 	}
 
+	val debugCollisions get() = world?.client?.settings?.debugCollisions ?: false
+
 	val transform = Matrix4f().translate(x = chunk.pos.x * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf, y = chunk.pos.x * CHUNK_SIZE * PIXELS_IN_STARBOUND_UNITf)
 
 	private val unloadableBakedMeshes = ArrayList<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) {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt
index 43fd64f3..ca6308a9 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt
@@ -301,7 +301,7 @@ class Font(
 				ebo.bind()
 				vbo.bind()
 
-				val builder = VertexBuilder(GLFlatAttributeList.VERTEX_2D_TEXTURE, VertexType.QUADS)
+				val builder = DynamicVertexBuilder(GLFlatAttributeList.VERTEX_2D_TEXTURE, VertexType.QUADS)
 
 				builder.quad(0f, 0f, width, height, VertexTransformers.uv())
 				builder.upload(vbo, ebo, GL_STATIC_DRAW)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt
index 1fe45b4c..b680d55a 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt
@@ -15,13 +15,13 @@ import kotlin.collections.HashMap
 
 data class TileLayer(
 	val bakedProgramState: BakedProgramState,
-	val vertexBuilder: VertexBuilder,
+	val vertexBuilder: DynamicVertexBuilder,
 	val zPos: Int)
 
 class TileLayerList {
 	private val layers = HashMap<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()
 	}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt
new file mode 100644
index 00000000..3879648e
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt
@@ -0,0 +1,476 @@
+package ru.dbotthepony.kstarbound.math
+
+import ru.dbotthepony.kstarbound.api.IStruct2d
+import ru.dbotthepony.kstarbound.world.ChunkPos
+import kotlin.math.absoluteValue
+
+data class IntersectionTime(
+	val invEntry: Vector2d,
+	val invExit: Vector2d,
+	val entry: Vector2d,
+	val exit: Vector2d,
+) {
+	companion object {
+		val ZERO = IntersectionTime(Vector2d.ZERO, Vector2d.ZERO, Vector2d.ZERO, Vector2d.ZERO)
+	}
+}
+
+data class SweepResult(
+	val normal: Vector2d,
+	val collisionTime: Double,
+	val intersectionTime: IntersectionTime
+) {
+	companion object {
+		val ZERO = SweepResult(Vector2d.ZERO, 1.0, IntersectionTime.ZERO)
+		val INTERSECT = SweepResult(Vector2d.ZERO, 0.0, IntersectionTime.ZERO)
+	}
+}
+
+/**
+ * Класс для описания Axis Aligned Bounding Box, двумя векторами,
+ * где [mins] - нижняя левая точка,
+ * [maxs] - верхняя правая
+ */
+data class AABB(val mins: Vector2d, val maxs: Vector2d) {
+	init {
+		require(mins.x < maxs.x) { "mins.x ${mins.x} is more or equal to maxs.x ${maxs.x}" }
+		require(mins.y < maxs.y) { "mins.y ${mins.y} is more or equal to maxs.y ${maxs.y}" }
+	}
+
+	operator fun plus(other: AABB) = AABB(mins + other.mins, maxs + other.maxs)
+	operator fun minus(other: AABB) = AABB(mins - other.mins, maxs - other.maxs)
+	operator fun times(other: AABB) = AABB(mins * other.mins, maxs * other.maxs)
+	operator fun div(other: AABB) = AABB(mins / other.mins, maxs / other.maxs)
+
+	operator fun plus(other: Vector2d) = AABB(mins + other, maxs + other)
+	operator fun minus(other: Vector2d) = AABB(mins - other, maxs - other)
+	operator fun times(other: Vector2d) = AABB(mins * other, maxs * other)
+	operator fun div(other: Vector2d) = AABB(mins / other, maxs / other)
+
+	operator fun plus(other: Double) = AABB(mins + other, maxs + other)
+	operator fun minus(other: Double) = AABB(mins - other, maxs - other)
+	operator fun times(other: Double) = AABB(mins * other, maxs * other)
+	operator fun div(other: Double) = AABB(mins / other, maxs / other)
+
+	val xSpan get() = maxs.x - mins.x
+	val ySpan get() = maxs.y - mins.y
+	val centre get() = mins + maxs * 0.5
+
+	val A get() = mins
+	val B get() = Vector2d(mins.x, maxs.y)
+	val C get() = maxs
+	val D get() = Vector2d(maxs.x, mins.y)
+
+	val bottomLeft get() = A
+	val topLeft get() = B
+	val topRight get() = C
+	val bottomRight get() = D
+
+	val width get() = (maxs.x - mins.x) / 2.0
+	val height get() = (maxs.y - mins.y) / 2.0
+
+	val diameter get() = mins.distance(maxs)
+	val radius get() = diameter / 2.0
+
+	fun isInside(point: Vector2d): Boolean {
+		return point.x in mins.x .. maxs.x && point.y in mins.y .. maxs.y
+	}
+
+	/**
+	 * Есть ли пересечение между этим AABB и [other]
+	 *
+	 * Считается, что они пересекаются, даже если у них просто равна одна из осей
+	 */
+	fun intersect(other: AABB): Boolean {
+		val intersectX: Boolean
+
+		if (xSpan <= other.xSpan)
+			intersectX = mins.x in other.mins.x .. other.maxs.x || maxs.x in other.mins.x .. other.maxs.x
+		else
+			intersectX = other.mins.x in mins.x .. maxs.x || other.maxs.x in mins.x .. maxs.x
+
+		if (!intersectX)
+			return false
+
+		val intersectY: Boolean
+
+		if (ySpan <= other.ySpan)
+			intersectY = mins.y in other.mins.y .. other.maxs.y || maxs.y in other.mins.y .. other.maxs.y
+		else
+			intersectY = other.mins.y in mins.y .. maxs.y || other.maxs.y in mins.y .. maxs.y
+
+		return intersectY
+	}
+
+	/**
+	 * Есть ли пересечение между этим AABB и [other]
+	 *
+	 * Считается, что они НЕ пересекаются, если у них просто равна одна из осей
+	 */
+	fun intersectWeak(other: AABB): Boolean {
+		if (maxs.x == other.mins.x || mins.x == other.maxs.x || maxs.y == other.mins.y || mins.y == other.maxs.y)
+			return false
+
+		val intersectX: Boolean
+
+		if (xSpan <= other.xSpan)
+			intersectX = mins.x in other.mins.x .. other.maxs.x || maxs.x in other.mins.x .. other.maxs.x
+		else
+			intersectX = other.mins.x in mins.x .. maxs.x || other.maxs.x in mins.x .. maxs.x
+
+		if (!intersectX)
+			return false
+
+		val intersectY: Boolean
+
+		if (ySpan <= other.ySpan)
+			intersectY = mins.y in other.mins.y .. other.maxs.y || maxs.y in other.mins.y .. other.maxs.y
+		else
+			intersectY = other.mins.y in mins.y .. maxs.y || other.maxs.y in mins.y .. maxs.y
+
+		return intersectY
+	}
+
+	fun intersectionDepth(other: AABB): Vector2d {
+		val xDepth: Double
+		val yDepth: Double
+
+		val thisCentre = centre
+		val otherCentre = other.centre
+
+		if (thisCentre.x > otherCentre.x) {
+			// считаем, что мы вошли справа
+			xDepth = mins.x - other.maxs.x
+		} else {
+			// считаем, что мы вошли слева
+			xDepth = maxs.x - other.mins.x
+		}
+
+		if (thisCentre.y > otherCentre.y) {
+			// считаем, что мы вошли сверху
+			yDepth = mins.y - other.maxs.y
+		} else {
+			// считаем, что мы вошли снизу
+			yDepth = maxs.x - other.mins.x
+		}
+
+		return Vector2d(xDepth, yDepth)
+	}
+
+	fun pushOutFrom(other: AABB): Vector2d {
+		if (!intersect(other))
+			return Vector2d.ZERO
+
+		val depth = intersectionDepth(other)
+
+		if (depth.x.absoluteValue < depth.y.absoluteValue) {
+			return Vector2d(x = depth.x)
+		} else {
+			return Vector2d(y = depth.y)
+		}
+	}
+
+	/**
+	 * Рассчитывает "время" пересечения
+	 *
+	 * https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/swept-aabb-collision-detection-and-response-r3084/
+	 *
+	 * Исправленный комментатором той же статьи от hypernewbie
+	 */
+	fun intersectionTime(other: AABB, velocity: Vector2d): IntersectionTime {
+		val xInvEntry: Double
+		val yInvEntry: Double
+		val xInvExit: Double
+		val yInvExit: Double
+
+		if (velocity.x > 0.0) {
+			xInvEntry = other.mins.x - maxs.x
+			xInvExit = other.maxs.x - mins.x
+		} else {
+			xInvEntry = other.maxs.x - mins.x
+			xInvExit = other.mins.x - maxs.x
+		}
+
+		if (velocity.y > 0.0) {
+			yInvEntry = other.mins.y - maxs.y
+			yInvExit = other.maxs.y - mins.y
+		} else {
+			yInvEntry = other.maxs.y - mins.y
+			yInvExit = other.mins.y - maxs.y
+		}
+
+		var xEntry: Double
+		var yEntry: Double
+		val xExit: Double
+		val yExit: Double
+
+		if (velocity.x == 0.0) {
+			xEntry = Double.NEGATIVE_INFINITY
+			xExit = Double.POSITIVE_INFINITY
+		} else {
+			xEntry = xInvEntry / velocity.x
+			xExit = xInvExit / velocity.x
+		}
+
+		if (velocity.y == 0.0) {
+			yEntry = Double.NEGATIVE_INFINITY
+			yExit = Double.POSITIVE_INFINITY
+		} else {
+			yEntry = yInvEntry / velocity.y
+			yExit = yInvExit / velocity.y
+		}
+
+		if (yEntry > 1.0) yEntry = Double.NEGATIVE_INFINITY
+		if (xEntry > 1.0) xEntry = Double.NEGATIVE_INFINITY
+
+		return IntersectionTime(
+			Vector2d(xInvEntry, yInvEntry),
+			Vector2d(xInvExit, yInvExit),
+			Vector2d(xEntry, yEntry),
+			Vector2d(xExit, yExit),
+		)
+	}
+
+	/**
+	 * Рассчитывает нормаль пересечения и процент пути ("время"), на котором произошло столкновение.
+	 *
+	 * Если столкновение не произошло, то возвращается [SweepResult.ZERO]
+	 *
+	 * Внимание: Если пересечение уже произошло (т.е. другой AABB пересекается с this), то данный метод
+	 * вернёт заведомо ложный результат (т.е. "нет пересечения")
+	 *
+	 * https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/swept-aabb-collision-detection-and-response-r3084/
+	 *
+	 * Исправленный комментатором той же статьи от hypernewbie
+	 */
+	fun sweep(other: AABB, velocity: Vector2d): SweepResult {
+		val time = intersectionTime(other, velocity)
+		val (near, far, entry, exit) = time
+		val (xEntry, yEntry) = entry
+		val (xExit, yExit) = exit
+
+		val entryTime = xEntry.coerceAtLeast(yEntry)
+		val exitTime = xExit.coerceAtLeast(yExit)
+
+		// гарантированно нет столкновения
+		if (entryTime > exitTime || xEntry < 0.0 && yEntry < 0.0) {
+			return SweepResult.ZERO
+		}
+
+		if (xEntry < 0.0) {
+			if (maxs.x < other.mins.x || mins.x > other.maxs.x)
+				return SweepResult.ZERO
+		}
+
+		if (yEntry < 0.0) {
+			if (maxs.y < other.mins.y || mins.y > other.maxs.y)
+				return SweepResult.ZERO
+		}
+
+		val (xInvEntry, yInvEntry) = near
+		val normal: Vector2d
+
+		if (xEntry > yEntry) {
+			if (xInvEntry < 0.0) {
+				normal = Vector2d.RIGHT
+			} else {
+				normal = Vector2d.LEFT
+			}
+		} else {
+			if (yInvEntry < 0.0) {
+				normal = Vector2d.UP
+			} else {
+				normal = Vector2d.DOWN
+			}
+		}
+
+		return SweepResult(normal, entryTime, time)
+	}
+
+	/**
+	 * Рассчитывает нормаль пересечения и процент пути ("время"), на котором произошло столкновение.
+	 *
+	 * Если столкновение не произошло, то возвращается [SweepResult.ZERO]
+	 *
+	 * Если данный AABB уже столкнулся с [other], возвращается [SweepResult.INTERSECT]
+	 *
+	 * https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/swept-aabb-collision-detection-and-response-r3084/
+	 */
+	fun safeSweep(other: AABB, velocity: Vector2d): SweepResult {
+		if (intersect(other)) {
+			return SweepResult.INTERSECT
+		}
+
+		return sweep(other, velocity)
+	}
+
+	fun encasingIntAABB(): AABBi {
+		return AABBi(
+			Vector2i(roundByAbsoluteValue(mins.x), roundByAbsoluteValue(mins.y)),
+			Vector2i(roundByAbsoluteValue(maxs.x), roundByAbsoluteValue(maxs.y)),
+		)
+	}
+
+	fun encasingChunkPosAABB(): AABBi {
+		return AABBi(
+			Vector2i(ChunkPos.tileToChunkComponent(roundByAbsoluteValue(mins.x)), ChunkPos.tileToChunkComponent(roundByAbsoluteValue(mins.y))),
+			Vector2i(ChunkPos.tileToChunkComponent(roundByAbsoluteValue(maxs.x)), ChunkPos.tileToChunkComponent(roundByAbsoluteValue(maxs.y))),
+		)
+	}
+
+	/**
+	 * Возвращает AABB, который содержит в себе оба AABB
+	 */
+	fun combine(other: AABB): AABB {
+		val minX = mins.x.coerceAtMost(other.mins.x)
+		val minY = mins.y.coerceAtMost(other.mins.y)
+		val maxX = maxs.x.coerceAtLeast(other.maxs.x)
+		val maxY = maxs.y.coerceAtLeast(other.maxs.y)
+
+		return AABB(Vector2d(minX, minY), Vector2d(maxX, maxY))
+	}
+
+	companion object {
+		fun rectangle(pos: IStruct2d, width: Double, height: Double = width): AABB {
+			val (x, y) = pos
+
+			return AABB(
+				Vector2d(x - width / 2.0, y - height / 2.0),
+				Vector2d(x + width / 2.0, y + height / 2.0),
+			)
+		}
+	}
+}
+
+data class AABBi(val mins: Vector2i, val maxs: Vector2i) {
+	init {
+		require(mins.x <= maxs.x) { "mins.x ${mins.x} is more than maxs.x ${maxs.x}" }
+		require(mins.y <= maxs.y) { "mins.y ${mins.y} is more than maxs.y ${maxs.y}" }
+	}
+
+	operator fun plus(other: AABBi) = AABBi(mins + other.mins, maxs + other.maxs)
+	operator fun minus(other: AABBi) = AABBi(mins - other.mins, maxs - other.maxs)
+	operator fun times(other: AABBi) = AABBi(mins * other.mins, maxs * other.maxs)
+	operator fun div(other: AABBi) = AABBi(mins / other.mins, maxs / other.maxs)
+
+	operator fun plus(other: Vector2i) = AABBi(mins + other, maxs + other)
+	operator fun minus(other: Vector2i) = AABBi(mins - other, maxs - other)
+	operator fun times(other: Vector2i) = AABBi(mins * other, maxs * other)
+	operator fun div(other: Vector2i) = AABBi(mins / other, maxs / other)
+
+	operator fun plus(other: Int) = AABBi(mins + other, maxs + other)
+	operator fun minus(other: Int) = AABBi(mins - other, maxs - other)
+	operator fun times(other: Int) = AABBi(mins * other, maxs * other)
+	operator fun div(other: Int) = AABBi(mins / other, maxs / other)
+
+	val xSpan get() = maxs.x - mins.x
+	val ySpan get() = maxs.y - mins.y
+	val centre get() = mins.toDoubleVector() + maxs.toDoubleVector() * 0.5
+
+	val A get() = mins
+	val B get() = Vector2i(mins.x, maxs.y)
+	val C get() = maxs
+	val D get() = Vector2i(maxs.x, mins.y)
+
+	val bottomLeft get() = A
+	val topLeft get() = B
+	val topRight get() = C
+	val bottomRight get() = D
+
+	val width get() = (maxs.x - mins.x) / 2
+	val height get() = (maxs.y - mins.y) / 2
+
+	val diameter get() = mins.distance(maxs)
+	val radius get() = diameter / 2.0
+
+	fun isInside(point: Vector2i): Boolean {
+		return point.x in mins.x .. maxs.x && point.y in mins.y .. maxs.y
+	}
+
+	/**
+	 * Есть ли пересечение между этим AABB и [other]
+	 *
+	 * Считается, что они пересекаются, даже если у них просто равна одна из осей
+	 */
+	fun intersect(other: AABBi): Boolean {
+		val intersectX: Boolean
+
+		if (xSpan <= other.xSpan)
+			intersectX = mins.x in other.mins.x .. other.maxs.x || maxs.x in other.mins.x .. other.maxs.x
+		else
+			intersectX = other.mins.x in mins.x .. maxs.x || other.maxs.x in mins.x .. maxs.x
+
+		if (!intersectX)
+			return false
+
+		val intersectY: Boolean
+
+		if (ySpan <= other.ySpan)
+			intersectY = mins.y in other.mins.y .. other.maxs.y || maxs.y in other.mins.y .. other.maxs.y
+		else
+			intersectY = other.mins.y in mins.y .. maxs.y || other.maxs.y in mins.y .. maxs.y
+
+		return intersectY
+	}
+
+	/**
+	 * Есть ли пересечение между этим AABB и [other]
+	 *
+	 * Считается, что они НЕ пересекаются, если у них просто равна одна из осей
+	 */
+	fun intersectWeak(other: AABBi): Boolean {
+		if (maxs.x == other.mins.x || mins.x == other.maxs.x || maxs.y == other.mins.y || mins.y == other.maxs.y)
+			return false
+
+		val intersectX: Boolean
+
+		if (xSpan <= other.xSpan)
+			intersectX = mins.x in other.mins.x .. other.maxs.x || maxs.x in other.mins.x .. other.maxs.x
+		else
+			intersectX = other.mins.x in mins.x .. maxs.x || other.maxs.x in mins.x .. maxs.x
+
+		if (!intersectX)
+			return false
+
+		val intersectY: Boolean
+
+		if (ySpan <= other.ySpan)
+			intersectY = mins.y in other.mins.y .. other.maxs.y || maxs.y in other.mins.y .. other.maxs.y
+		else
+			intersectY = other.mins.y in mins.y .. maxs.y || other.maxs.y in mins.y .. maxs.y
+
+		return intersectY
+	}
+
+	fun toDoubleAABB() = AABB(mins.toDoubleVector(), maxs.toDoubleVector())
+
+	private inner class Iterator<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)
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Matrix.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Matrix.kt
index 94b54c0b..47dd09ed 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Matrix.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Matrix.kt
@@ -21,6 +21,10 @@ interface IMatrixLikeFloat : IMatrixLike {
 	operator fun get(row: Int, column: Int): Float
 }
 
+interface IMatrixLikeDouble : IMatrixLike {
+	operator fun get(row: Int, column: Int): Double
+}
+
 interface IMatrix : IMatrixLike {
 	operator fun plus(other: IMatrix): IMatrix
 	operator fun minus(other: IMatrix): IMatrix
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt
new file mode 100644
index 00000000..393367ab
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt
@@ -0,0 +1,27 @@
+package ru.dbotthepony.kstarbound.math
+
+fun lerp(t: Double, a: Double, b: Double): Double {
+	return a * (1.0 - t) + b * t
+}
+
+/**
+ * Выполняет преобразование [value] типа [Double] в [Int] так,
+ * что выходной [Int] всегда будет больше или равен по модулю [value]
+ */
+fun roundByAbsoluteValue(value: Double): Int {
+	if (value > 0.0) {
+		if (value % 1.0 != 0.0) {
+			return value.toInt() + 1
+		}
+
+		return value.toInt()
+	} else if (value == -0.0 || value == 0.0) {
+		return 0
+	} else {
+		if (value % 1.0 != -0.0) {
+			return value.toInt() - 1
+		}
+
+		return value.toInt()
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt
index 6ab73611..158ace54 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt
@@ -1,12 +1,17 @@
 package ru.dbotthepony.kstarbound.math
 
 import com.google.gson.JsonArray
-import ru.dbotthepony.kstarbound.api.IStruct2f
-import ru.dbotthepony.kstarbound.api.IStruct2i
-import ru.dbotthepony.kstarbound.api.IStruct3f
-import ru.dbotthepony.kstarbound.api.IStruct4f
+import ru.dbotthepony.kstarbound.api.*
 import kotlin.math.cos
+import kotlin.math.pow
 import kotlin.math.sin
+import kotlin.math.sqrt
+
+// Так как у нас нет шаблонов ни в Java, ни в Kotlin
+// а дженерики вызывают autoboxing
+// приходится создавать "бетонные" реализации для каждого вида вектора
+
+// а ведь компилятор мог бы это генерировать.
 
 abstract class IVector2i<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
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Formatter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Formatter.kt
index 8bd718e1..f1836c40 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Formatter.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Formatter.kt
@@ -9,8 +9,8 @@ private const val PETIBYTE = TEBIBYTE * 1024L
 fun formatBytesShort(input: Long): String {
 	return when (input) {
 		in 0 until KIBIBYTE -> "${input}b"
-		in KIBIBYTE until MEBIBYTE -> "%.2fKiB".format((input / KIBIBYTE).toDouble() + (input % KIBIBYTE).toDouble() / KIBIBYTE)
-		in MEBIBYTE until GIBIBYTE -> "%.2fMiB".format((input / MEBIBYTE).toDouble() + (input % MEBIBYTE).toDouble() / MEBIBYTE)
+		in KIBIBYTE until MEBIBYTE -> "${(((input / KIBIBYTE).toDouble() + (input % KIBIBYTE).toDouble() / KIBIBYTE) * 100.0).toLong().toDouble() / 100.0}KiB"
+		in MEBIBYTE until GIBIBYTE -> "${(((input / MEBIBYTE).toDouble() + (input % MEBIBYTE).toDouble() / MEBIBYTE) * 100.0).toLong().toDouble() / 100.0}MiB"
 		else -> "${input}b"
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt
index 3aa2958d..fae159c2 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt
@@ -2,8 +2,12 @@ package ru.dbotthepony.kstarbound.world
 
 import ru.dbotthepony.kstarbound.api.IStruct2i
 import ru.dbotthepony.kstarbound.defs.TileDefinition
+import ru.dbotthepony.kstarbound.math.AABB
 import ru.dbotthepony.kstarbound.math.IVector2i
+import ru.dbotthepony.kstarbound.math.Vector2d
 import ru.dbotthepony.kstarbound.math.Vector2i
+import java.util.*
+import kotlin.collections.ArrayList
 
 /**
  * Представляет из себя класс, который содержит состояние тайла на заданной позиции
@@ -146,7 +150,10 @@ interface IMutableTileChunk : ITileChunk, ITileSetter
 
 const val CHUNK_SHIFT = 5
 const val CHUNK_SIZE = 1 shl CHUNK_SHIFT // 32
+
 const val CHUNK_SIZE_FF = CHUNK_SIZE - 1
+const val CHUNK_SIZEf = CHUNK_SIZE.toFloat()
+const val CHUNK_SIZEd = CHUNK_SIZE.toDouble()
 
 data class ChunkPos(override val x: Int, override val y: Int) : IVector2i<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()),
+		)
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
index eb08aad1..2a3f1a3e 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
@@ -1,6 +1,9 @@
 package ru.dbotthepony.kstarbound.world
 
+import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT
 import ru.dbotthepony.kstarbound.defs.TileDefinition
+import ru.dbotthepony.kstarbound.math.AABBi
+import ru.dbotthepony.kstarbound.math.Vector2d
 import ru.dbotthepony.kstarbound.math.Vector2i
 
 /**
@@ -40,11 +43,19 @@ open class MutableWorldChunkTuple(
 	override var bottom: IWorldChunkTuple?,
 ) : IMutableWorldChunkTuple
 
-@Suppress("WeakerAccess")
+const val EARTH_FREEFALL_ACCELERATION = 9.8312 / METRES_IN_STARBOUND_UNIT
+
 abstract class World<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
+	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt
new file mode 100644
index 00000000..106119f0
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt
@@ -0,0 +1,8 @@
+package ru.dbotthepony.kstarbound.world.entities
+
+import ru.dbotthepony.kstarbound.world.World
+
+open class AliveEntity(world: World<*>) : Entity(world) {
+	var maxHealth = 10.0
+	var health = 10.0
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt
new file mode 100644
index 00000000..bb925bda
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt
@@ -0,0 +1,179 @@
+package ru.dbotthepony.kstarbound.world.entities
+
+import org.apache.logging.log4j.LogManager
+import ru.dbotthepony.kstarbound.math.AABB
+import ru.dbotthepony.kstarbound.math.Vector2d
+import ru.dbotthepony.kstarbound.math.lerp
+import ru.dbotthepony.kstarbound.world.Chunk
+import ru.dbotthepony.kstarbound.world.World
+import kotlin.math.absoluteValue
+
+enum class CollisionResolution {
+	STOP,
+	BOUNCE,
+	PUSH,
+	SLIDE,
+}
+
+/**
+ * Определяет из себя сущность в мире, которая имеет позицию, скорость и коробку столкновений
+ */
+open class Entity(val world: World<*>) {
+	var chunk: Chunk? = null
+		protected set
+
+	val worldaabb get() = aabb + pos
+	var pos = Vector2d()
+	var velocity = Vector2d()
+
+	/**
+	 * Касается ли сущность земли
+	 *
+	 * Данный флаг выставляется при обработке скорости, если данный флаг не будет выставлен
+	 * правильно, то сущность будет иметь очень плохое движение в стороны
+	 *
+	 * Так же от него зависит то, может ли сущность двигаться, если она не парит
+	 *
+	 * Если сущность касается земли, то на неё не действует гравитация
+	 */
+	var onGround = false
+		protected set
+
+	// наследуемые свойства
+	open val aabb = AABB.rectangle(Vector2d.ZERO, 0.9, 0.9)
+	open val affectedByGravity = true
+	open val collisionResolution = CollisionResolution.STOP
+
+	protected open fun onTouchGround(velocity: Vector2d, normal: Vector2d) {
+
+	}
+
+	open fun propagateVelocity(delta: Double) {
+		if (velocity.length == 0.0)
+			return
+
+		var deltaMovement = velocity * delta
+
+		var potentialAABB = worldaabb + deltaMovement
+		var combined = worldaabb.combine(potentialAABB)
+		val collected = world.collect((combined).encasingChunkPosAABB()).map { it.chunk.foreground.collisionLayers() }
+
+		if (collected.isNotEmpty()) {
+			var newOnGround = false
+
+			for (iteration in 0 .. 100) {
+				var collided = false
+
+				for (aabbList in collected) {
+					for (aabb in aabbList) {
+						if (!newOnGround && iteration == 0) {
+							if (worldaabb.sweep(aabb, world.gravity * delta).collisionTime < 1.0) {
+								newOnGround = true
+							}
+						}
+
+						// залез в блоки?
+						if (potentialAABB.intersectWeak(aabb)) {
+							val push = worldaabb.pushOutFrom(aabb)
+
+							if (push.length > 0.0) {
+								velocity -= push * delta * 100.0
+								deltaMovement = velocity * delta
+								potentialAABB = worldaabb + deltaMovement
+								combined = worldaabb.combine(potentialAABB)
+
+								onGround = true
+								collided = true
+								continue
+							}
+						}
+
+						// ранний тест (отсечение заведомо не пересекаемой геометрии)
+						if (!aabb.intersect(combined)) {
+							continue
+						}
+
+						// обычный тест коллизии
+						val (normal, collisionTime) = worldaabb.sweep(aabb, deltaMovement)
+
+						if (collisionTime != 1.0) {
+							val remainingTime = 1.0 - collisionTime
+							val oldVelocity = velocity
+
+							when (collisionResolution) {
+								CollisionResolution.STOP -> {
+									velocity *= remainingTime
+								}
+
+								CollisionResolution.PUSH -> {
+									var dot = deltaMovement.invDotProduct(normal)
+									val magnitude = deltaMovement.length * remainingTime
+
+									if (dot > 0.0) {
+										dot = 1.0
+									} else {
+										dot = -1.0
+									}
+
+									velocity = Vector2d(dot * normal.y * magnitude, dot * normal.x * magnitude) / delta
+								}
+
+								CollisionResolution.SLIDE -> {
+									val dot = deltaMovement.invDotProduct(normal) * remainingTime
+									velocity = Vector2d(dot * normal.y, dot * normal.x) / delta
+								}
+
+								CollisionResolution.BOUNCE -> {
+									velocity *= remainingTime
+
+									if (normal.x.absoluteValue > 0.00001 && normal.y.absoluteValue > 0.00001) {
+										velocity *= Vector2d.INVERT_XY
+									} else if (normal.x.absoluteValue > 0.00001) {
+										velocity *= Vector2d.INVERT_X
+									} else if (normal.y.absoluteValue > 0.00001) {
+										velocity *= Vector2d.INVERT_Y
+									}
+								}
+							}
+
+							collided = true
+
+							if (!newOnGround) {
+								newOnGround = normal.dotProduct(world.gravity) <= -0.98
+							}
+
+							deltaMovement = velocity * delta
+							potentialAABB = worldaabb + deltaMovement
+							onTouchGround(oldVelocity, normal)
+						}
+					}
+				}
+
+				if (!collided) {
+					//println("Resolved collision on $iteration")
+					break
+				}
+			}
+
+			onGround = newOnGround
+			//println(newOnGround)
+		} else {
+			onGround = false
+		}
+
+		pos += velocity * delta
+	}
+
+	open fun moveAndCollide(delta: Double) {
+		if (!onGround && affectedByGravity)
+			velocity += world.gravity * delta
+		else if (affectedByGravity)
+			velocity *= Vector2d(lerp(delta, 1.0, 0.01), 1.0)
+
+		propagateVelocity(delta)
+	}
+
+	companion object {
+		private val LOGGER = LogManager.getLogger(Entity::class.java)
+	}
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Humanoid.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Humanoid.kt
new file mode 100644
index 00000000..41258bb1
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Humanoid.kt
@@ -0,0 +1,10 @@
+package ru.dbotthepony.kstarbound.world.entities
+
+import ru.dbotthepony.kstarbound.math.AABB
+import ru.dbotthepony.kstarbound.math.Vector2d
+import ru.dbotthepony.kstarbound.world.World
+
+open class Humanoid(world: World<*>) : AliveEntity(world) {
+	override val aabb = AABB.rectangle(Vector2d.ZERO, 1.8, 3.7)
+	override val collisionResolution = CollisionResolution.SLIDE
+}
diff --git a/src/main/resources/shaders/fragment/flat_color.glsl b/src/main/resources/shaders/fragment/flat_color.glsl
new file mode 100644
index 00000000..809cabd8
--- /dev/null
+++ b/src/main/resources/shaders/fragment/flat_color.glsl
@@ -0,0 +1,9 @@
+
+#version 460
+
+uniform vec4 _color;
+out vec4 _color_out;
+
+void main() {
+	_color_out = _color;
+}
diff --git a/src/main/resources/shaders/vertex/flat_vertex_2d.glsl b/src/main/resources/shaders/vertex/flat_vertex_2d.glsl
new file mode 100644
index 00000000..dfec2e1f
--- /dev/null
+++ b/src/main/resources/shaders/vertex/flat_vertex_2d.glsl
@@ -0,0 +1,9 @@
+
+#version 460
+
+layout (location = 0) in vec2 _pos;
+uniform mat4 _transform;
+
+void main() {
+	gl_Position = _transform * vec4(_pos, 0.5, 1.0);
+}
diff --git a/src/test/kotlin/ru/dbotthepony/kstarbound/test/MathTests.kt b/src/test/kotlin/ru/dbotthepony/kstarbound/test/MathTests.kt
new file mode 100644
index 00000000..fb1b7089
--- /dev/null
+++ b/src/test/kotlin/ru/dbotthepony/kstarbound/test/MathTests.kt
@@ -0,0 +1,51 @@
+package ru.dbotthepony.kstarbound.test
+
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+import ru.dbotthepony.kstarbound.math.AABB
+import ru.dbotthepony.kstarbound.math.Vector2d
+import ru.dbotthepony.kstarbound.math.roundByAbsoluteValue
+
+object MathTests {
+	@Test
+	@DisplayName("roundByAbsoluteValue test")
+	fun roundByAbsoluteValueTest() {
+		check(roundByAbsoluteValue(0.0) == 0)
+		check(roundByAbsoluteValue(0.1) == 1)
+		check(roundByAbsoluteValue(1.1) == 2)
+		check(roundByAbsoluteValue(-0.1) == -1)
+		check(roundByAbsoluteValue(-0.0) == 0)
+		check(roundByAbsoluteValue(-1.0) == -1)
+		check(roundByAbsoluteValue(-1.1) == -2)
+	}
+
+	@Test
+	@DisplayName("AABB Basic Math")
+	fun basicAABB() {
+		val a = AABB.rectangle(Vector2d.ZERO, 1.0, 1.0)
+
+		check(a.intersect(AABB.rectangle(Vector2d(-1.0), 1.0, 1.0)))
+		check(!a.intersectWeak(AABB.rectangle(Vector2d(-1.0), 1.0, 1.0)))
+		check(!a.intersect(AABB.rectangle(Vector2d(-2.0), 1.0, 1.0)))
+		check(!a.intersectWeak(AABB.rectangle(Vector2d(-2.0), 1.0, 1.0)))
+
+		check(a.intersect(AABB.rectangle(Vector2d(-0.9), 1.0, 1.0)))
+		check(a.intersectWeak(AABB.rectangle(Vector2d(-0.9), 1.0, 1.0)))
+
+		val bigA = AABB.rectangle(Vector2d.ZERO, 200.0, 200.0)
+		val smallB = AABB.rectangle(Vector2d.ZERO, 1.0, 1.0)
+
+		check(bigA.intersect(smallB))
+		check(smallB.intersect(bigA))
+
+		check(bigA.intersectWeak(smallB))
+		check(smallB.intersectWeak(bigA))
+
+		check(AABB.rectangle(Vector2d.ZERO, 1.0, 1.0) == AABB(Vector2d(-0.5, -0.5), Vector2d(0.5, 0.5)))
+
+		val combineA = AABB(Vector2d(0.0, 0.0), Vector2d(2.0, 2.0))
+		val combineB = AABB(Vector2d(2.0, 5.0), Vector2d(4.0, 6.0))
+
+		check(combineA.combine(combineB) == AABB(Vector2d(0.0, 0.0), Vector2d(4.0, 6.0))) { combineA.combine(combineB).toString() }
+	}
+}