diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt
index deb45ac3..516755f2 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt
@@ -15,9 +15,13 @@ import ru.dbotthepony.kstarbound.world.*
 import ru.dbotthepony.kstarbound.world.entities.Entity
 import ru.dbotthepony.kvector.util2d.AABB
 import ru.dbotthepony.kvector.vector.Color
+import ru.dbotthepony.kvector.vector.ndouble.Vector2d
 import ru.dbotthepony.kvector.vector.nfloat.Vector2f
 import ru.dbotthepony.kvector.vector.nint.Vector2i
+import kotlin.math.PI
+import kotlin.math.cos
 import kotlin.math.roundToInt
+import kotlin.math.sin
 
 class ClientWorld(
 	val client: StarboundClient,
@@ -146,9 +150,9 @@ class ClientWorld(
 
 		client.gl.blendFunc = old
 
-		val pos = client.screenToWorld(client.mouseCoordinatesF)
+		val pos = client.screenToWorld(client.mouseCoordinatesF).toDoubleVector()
 
-		val lightsize = 16
+		/*val lightsize = 16
 
 		val lightmap = floodLight(
 			Vector2i(pos.x.roundToInt(), pos.y.roundToInt()), lightsize
@@ -162,8 +166,44 @@ class ClientWorld(
 					}
 				}
 			}
+		}*/
+
+		/*
+		val rayFan = ArrayList<Vector2d>()
+
+		for (i in 0 .. 359) {
+			rayFan.add(Vector2d(cos(i / 180.0 * PI), sin(i / 180.0 * PI)))
 		}
 
+		for (ray in rayFan) {
+			val trace = castRayNaive(pos, ray, 16.0)
+
+			client.gl.quadWireframe {
+				for ((tpos, tile) in trace.traversedTiles) {
+					if (tile.material != null)
+						it.quad(
+							tpos.x.toFloat(),
+							tpos.y.toFloat(),
+							tpos.x + 1f,
+							tpos.y + 1f
+						)
+				}
+			}
+		}
+		 */
+
+		client.gl.quadWireframe {
+			for ((intensity, tpos) in rayLightCircleNaive(pos, 24.0, falloffByTravel = 1.5, falloffByTile = 4.0)) {
+				it.quad(
+					tpos.x.toFloat(),
+					tpos.y.toFloat(),
+					tpos.x + 1f,
+					tpos.y + 1f
+				)
+			}
+		}
+
+
 		physics.debugDraw()
 
 		/*for (renderer in determineRenderers) {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
index bb7ddaef..5ab2c03f 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt
@@ -1,5 +1,7 @@
 package ru.dbotthepony.kstarbound.world
 
+import com.google.common.collect.ImmutableList
+import it.unimi.dsi.fastutil.objects.Object2DoubleAVLTreeMap
 import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap
 import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
 import ru.dbotthepony.kbox2d.api.ContactImpulse
@@ -10,7 +12,6 @@ import ru.dbotthepony.kbox2d.dynamics.B2Fixture
 import ru.dbotthepony.kbox2d.dynamics.B2World
 import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact
 import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT
-import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
 import ru.dbotthepony.kstarbound.math.*
 import ru.dbotthepony.kstarbound.util.Timer
 import ru.dbotthepony.kstarbound.world.entities.CollisionResolution
@@ -21,7 +22,11 @@ import ru.dbotthepony.kvector.util2d.AABB
 import ru.dbotthepony.kvector.util2d.AABBi
 import ru.dbotthepony.kvector.vector.ndouble.Vector2d
 import ru.dbotthepony.kvector.vector.nint.Vector2i
+import kotlin.math.PI
 import kotlin.math.absoluteValue
+import kotlin.math.cos
+import kotlin.math.roundToInt
+import kotlin.math.sin
 
 const val EARTH_FREEFALL_ACCELERATION = 9.8312 / METRES_IN_STARBOUND_UNIT
 
@@ -34,6 +39,89 @@ data class WorldSweepResult(
 
 private const val EPSILON = 0.00001
 
+data class RayCastResult(
+	val traversedTiles: List<Pair<Vector2i, ITileState>>,
+	val hitTile: Pair<Vector2i, ITileState>?,
+	val fraction: Double
+)
+
+private fun makeDirFan(step: Double): List<Vector2d> {
+	var i = 0.0
+	val result = ImmutableList.builder<Vector2d>()
+
+	while (i < 360.0) {
+		i += step
+		result.add(Vector2d(cos(i / 180.0 * PI), sin(i / 180.0 * PI)))
+	}
+
+	return result.build()
+}
+
+private val potatoDirFan by lazy { makeDirFan(4.0) }
+private val veryRoughDirFan by lazy { makeDirFan(3.0) }
+private val roughDirFan by lazy { makeDirFan(2.0) }
+private val dirFan by lazy { makeDirFan(1.0) }
+private val preciseFan by lazy { makeDirFan(0.5) }
+private val veryPreciseFan by lazy { makeDirFan(0.25) }
+
+private fun chooseLightRayFan(size: Double): List<Vector2d> {
+	return when (size) {
+		in 0.0 .. 8.0 -> potatoDirFan
+		in 8.0 .. 12.0 -> veryRoughDirFan
+		in 12.0 .. 18.0 -> roughDirFan
+		in 18.0 .. 24.0 -> dirFan
+		in 24.0 .. 32.0 -> preciseFan
+		// in 32.0 .. 48.0 -> veryPreciseFan
+		else -> veryPreciseFan
+	}
+}
+
+/**
+ * [HIT] - луч попал по объекту и трассировка прекращается; объект записывается в коллекцию объектов, в которые попал луч.
+ *
+ * [HIT_SKIP] - луч попал по объекту и трассировка прекращается; объект не записывается в коллекцию объектов, в которые попал луч.
+ *
+ * [SKIP] - луч не попал по объекту, объект не записывается в коллекцию объектов, в которые попал луч.
+ *
+ * [CONTINUE] - луч не попал по объекту; объект записывается в коллекцию объектов, в которые попал луч.
+ */
+enum class RayFilterResult {
+	HIT,
+	HIT_SKIP,
+	SKIP,
+	CONTINUE;
+
+	companion object {
+		fun of(boolean: Boolean): RayFilterResult {
+			return if (boolean) HIT else CONTINUE
+		}
+	}
+}
+
+fun interface TileRayFilter {
+	fun test(state: ITileState, fraction: Double, position: Vector2i): RayFilterResult
+}
+
+/**
+ * Считает все тайлы неблокирующими
+ */
+val AnythingRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.CONTINUE }
+
+/**
+ * Попадает по первому не-пустому тайлу
+ */
+val NonSolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.material != null) }
+
+/**
+ * Попадает по первому пустому тайлу
+ */
+val SolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.material == null) }
+
+/**
+ * Попадает по первому тайлу который блокирует проход света
+ */
+val LineOfSightRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.material?.renderParameters?.lightTransparent == false) }
+
 abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(
 	val seed: Long,
 	val widthInChunks: Int
@@ -58,9 +146,6 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
 	 */
 	val dirtyPhysicsChunks = HashSet<ChunkType>()
 
-	protected var lastAccessedChunk: ChunkType? = null
-	protected var lastAccessedChunkPos: ChunkPos? = null
-
 	val physics = B2World(Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION))
 
 	private var timers = ArrayList<Timer>()
@@ -230,25 +315,12 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
 	 */
 	open operator fun get(pos: ChunkPos): ChunkType? {
 		if (!isCircular) {
-			//if (lastAccessedChunkPos == pos) {
-			//	return lastAccessedChunk
-			//}
-
-			//lastAccessedChunkPos = pos
-			lastAccessedChunk = chunkMap[pos]
-			return this.lastAccessedChunk
+			return chunkMap[pos]
 		}
 
 		@Suppress("Name_Shadowing")
 		val pos = pos.circular(widthInChunks)
-
-		if (lastAccessedChunkPos == pos) {
-			return lastAccessedChunk
-		}
-
-		val load = chunkMap[pos]
-		this.lastAccessedChunk = load
-		return load
+		return chunkMap[pos]
 	}
 
 	open fun getInstantTuple(pos: ChunkPos): IWorldChunkTuple<This, ChunkType>? {
@@ -262,18 +334,9 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
 		@Suppress("Name_Shadowing")
 		val pos = if (isCircular) pos.circular(widthInChunks) else pos
 
-		val _lastAccessedChunk = lastAccessedChunk
-
-		if (_lastAccessedChunk?.pos == pos) {
-			return _lastAccessedChunk
-		}
-
 		return chunkMap.computeIfAbsent(pos, Object2ObjectFunction {
 			val chunk = chunkFactory(pos)
 
-			lastAccessedChunk = chunk
-			lastAccessedChunkPos = pos
-
 			val orphanedInThisChunk = ArrayList<Entity>()
 
 			for (ent in orphanedEntities) {
@@ -292,6 +355,215 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
 		})
 	}
 
+	/**
+	 * Позволяет получать чанки/тайлы с минимальным кешем. Если один чанк считывается очень большое число раз,
+	 * то использование этого класса сильно ускорит работу.
+	 *
+	 * Так же реализует raycasting методы.
+	 */
+	inner class CachedGetter {
+		private var lastChunk: ChunkType? = null
+		private var lastPos: ChunkPos? = null
+
+		operator fun get(pos: ChunkPos): ChunkType? {
+			if (lastPos == pos) {
+				return lastChunk
+			}
+
+			lastChunk = this@World[pos]
+			lastPos = pos
+			return lastChunk
+		}
+
+		fun getTile(pos: Vector2i): ITileState? {
+			return get(ChunkPos.fromTilePosition(pos))?.foreground?.get(ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y))
+		}
+
+		fun getBackgroundTile(pos: Vector2i): ITileState? {
+			return get(ChunkPos.fromTilePosition(pos))?.background?.get(ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y))
+		}
+
+		/**
+		 * Бросает луч напротив тайлов мира с заданными позициями и фильтром
+		 */
+		fun castRayNaive(
+			rayStart: Vector2d,
+			rayEnd: Vector2d,
+			filter: TileRayFilter = AnythingRayFilter
+		): RayCastResult {
+			if (rayStart == rayEnd) {
+				return RayCastResult(listOf(), null, 1.0)
+			}
+
+			var t = 0.0
+			val dir = rayEnd - rayStart
+			val inc = 0.5 / dir.length
+
+			val tiles = ArrayList<Pair<Vector2i, ITileState>>()
+			var prev = Vector2i(Int.MIN_VALUE, Int.MAX_VALUE)
+			var hitTile: Pair<Vector2i, ITileState>? = null
+
+			while (t < 1.0) {
+				val (x, y) = rayStart + dir * t
+				val tilePos = Vector2i(x.roundToInt(), y.roundToInt())
+
+				if (tilePos != prev) {
+					val tile = getTile(tilePos) ?: EmptyTileState
+
+					when (filter.test(tile, t, tilePos)) {
+						RayFilterResult.HIT -> {
+							hitTile = tilePos to tile
+							tiles.add(hitTile)
+							break
+						}
+
+						RayFilterResult.HIT_SKIP -> {
+							hitTile = tilePos to tile
+							break
+						}
+
+						RayFilterResult.SKIP -> {}
+						RayFilterResult.CONTINUE -> tiles.add(tilePos to tile)
+					}
+
+					prev = tilePos
+				}
+
+				t += inc
+			}
+
+			return RayCastResult(tiles, hitTile, t)
+		}
+
+		/**
+		 * Бросает луч напротив тайлов мира с заданной позицией, направлением и фильтром
+		 */
+		fun castRayNaive(
+			rayPosition: Vector2d,
+			direction: Vector2d,
+			length: Double,
+			filter: TileRayFilter = AnythingRayFilter
+		): RayCastResult {
+			return castRayNaive(rayPosition, rayPosition + direction.normalized * length, filter)
+		}
+
+		/**
+		 * Выпускает луч света с заданной силой (определяет длину луча и способность проходить сквозь тайлы), позицией и направлением.
+		 *
+		 * Позволяет указать отдельно [falloffByTile] потерю силы света при прохождении через тайлы.
+		 */
+		fun rayLightNaive(
+			position: Vector2d,
+			direction: Vector2d,
+			intensity: Double,
+			falloffByTile: Double = 2.0,
+			falloffByTravel: Double = 1.0,
+		): List<Pair<Double, Vector2i>> {
+			val result = ArrayList<Pair<Double, Vector2i>>()
+
+			var currentIntensity = intensity
+
+			castRayNaive(position, direction, intensity) { state, t, pos ->
+				if (state.material?.renderParameters?.lightTransparent == false) {
+					currentIntensity -= falloffByTile
+				} else {
+					currentIntensity -= falloffByTravel
+				}
+
+				result.add(currentIntensity to pos)
+
+				if (currentIntensity <= 0.0) {
+					return@castRayNaive RayFilterResult.HIT
+				} else {
+					return@castRayNaive RayFilterResult.CONTINUE
+				}
+			}
+
+			return result
+		}
+
+		fun rayLightCircleNaive(
+			position: Vector2d,
+			intensity: Double,
+			falloffByTile: Double = 2.0,
+			falloffByTravel: Double = 2.0,
+		): List<Pair<Double, Vector2i>> {
+			val result = Object2DoubleAVLTreeMap<Vector2i> { a, b ->
+				val cmp = a.x.compareTo(b.x)
+
+				if (cmp != 0) {
+					return@Object2DoubleAVLTreeMap cmp
+				}
+
+				return@Object2DoubleAVLTreeMap a.y.compareTo(b.y)
+			}
+
+			result.defaultReturnValue(-1.0)
+
+			for (dir in chooseLightRayFan(intensity)) {
+				val result2 = rayLightNaive(position, dir, intensity, falloffByTile, falloffByTravel)
+
+				for (pair in result2) {
+					val existing = result.getDouble(pair.second)
+
+					if (existing < pair.first) {
+						result.put(pair.second, pair.first)
+					}
+				}
+			}
+
+			return result.map { it.value to it.key }
+		}
+	}
+
+	/**
+	 * @see CachedGetter.castRayNaive
+	 */
+	fun castRayNaive(
+		rayStart: Vector2d,
+		rayEnd: Vector2d,
+		filter: TileRayFilter = AnythingRayFilter
+	): RayCastResult {
+		return CachedGetter().castRayNaive(rayStart, rayEnd, filter)
+	}
+
+	/**
+	 * @see CachedGetter.castRayNaive
+	 */
+	fun castRayNaive(
+		rayPosition: Vector2d,
+		direction: Vector2d,
+		length: Double,
+		filter: TileRayFilter = AnythingRayFilter
+	): RayCastResult {
+		return CachedGetter().castRayNaive(rayPosition, direction, length, filter)
+	}
+
+	/**
+	 * @see CachedGetter.rayLightNaive
+	 */
+	fun rayLightNaive(
+		position: Vector2d,
+		direction: Vector2d,
+		intensity: Double,
+		falloffByTile: Double = 2.0,
+		falloffByTravel: Double = 1.0,
+	): List<Pair<Double, Vector2i>> {
+		return CachedGetter().rayLightNaive(position, direction, intensity, falloffByTile, falloffByTravel)
+	}
+
+	/**
+	 * @see CachedGetter.rayLightCircleNaive
+	 */
+	fun rayLightCircleNaive(
+		position: Vector2d,
+		intensity: Double,
+		falloffByTile: Double = 2.0,
+		falloffByTravel: Double = 2.0,
+	): List<Pair<Double, Vector2i>> {
+		return CachedGetter().rayLightCircleNaive(position, intensity, falloffByTile, falloffByTravel)
+	}
+
 	open fun getForegroundView(pos: ChunkPos): TileView {
 		val tuple = get(pos)?.let { InstantWorldChunkTuple(this as This, it) }
 
@@ -661,7 +933,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
 
 		val view = getRigidForegroundView(ChunkPos.fromTilePosition(lightPosition))
 
-		val calls = floodLightInto(
+		floodLightInto(
 			lightmap,
 			view,
 			lightIntensity,
@@ -672,8 +944,6 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
 			lightPosition.y - view.pos.tileY,
 		)
 
-		println(calls)
-
 		return lightmap
 	}
 }