package ru.dbotthepony.kstarbound.world import com.google.common.collect.ImmutableList import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.IChunkCell import ru.dbotthepony.kvector.arrays.Double2DArray import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2i import java.util.* import kotlin.collections.ArrayList import kotlin.math.PI import kotlin.math.cos import kotlin.math.roundToInt import kotlin.math.sin const val EARTH_FREEFALL_ACCELERATION = 9.8312 / METRES_IN_STARBOUND_UNIT data class RayCastResult( val traversedTiles: List>, val hitTile: Pair?, val fraction: Double ) private fun makeDirFan(step: Double): List { var i = 0.0 val result = ImmutableList.builder() 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 { return when (size) { in 0.0 .. 8.0 -> potatoDirFan in 8.0 .. 16.0 -> veryRoughDirFan in 16.0 .. 24.0 -> roughDirFan in 24.0 .. 48.0 -> dirFan in 48.0 .. 96.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: IChunkCell, fraction: Double, position: Vector2i): RayFilterResult } /** * Считает все тайлы неблокирующими */ val AnythingRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.CONTINUE } /** * Попадает по первому не-пустому тайлу */ val NonSolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material != null) } /** * Попадает по первому пустому тайлу */ val SolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material == null) } /** * Попадает по первому тайлу который блокирует проход света */ val LineOfSightRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material?.renderParameters?.lightTransparent == false) } /** * Бросает луч напротив тайлов мира с заданными позициями и фильтром */ fun ICellAccess.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 = LinkedList>() var prev = Vector2i(Int.MIN_VALUE, Int.MAX_VALUE) var hitTile: Pair? = null while (t < 1.0) { val (x, y) = rayStart + dir * t val tilePos = Vector2i(x.roundToInt(), y.roundToInt()) if (tilePos != prev) { val tile = getCell(tilePos) 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 ICellAccess.castRayNaive( rayPosition: Vector2d, direction: Vector2d, length: Double, filter: TileRayFilter = AnythingRayFilter ): RayCastResult { return castRayNaive(rayPosition, rayPosition + direction.unitVector * length, filter) } /** * Выпускает луч света с заданной силой (определяет длину луча и способность проходить сквозь тайлы), позицией и направлением. * * Позволяет указать отдельно [falloffByTile] потерю силы света при прохождении через тайлы. */ fun ICellAccess.rayLightNaive( position: Vector2d, direction: Vector2d, intensity: Double, falloffByTile: Double = 2.0, falloffByTravel: Double = 1.0, ): List> { val result = ArrayList>() var currentIntensity = intensity castRayNaive(position, direction, intensity) { state, t, pos -> if (state.foreground.material?.renderParameters?.lightTransparent == false) { currentIntensity -= falloffByTile } else { currentIntensity -= falloffByTravel } //result.add(currentIntensity to pos) if (currentIntensity <= 0.0) { return@castRayNaive RayFilterResult.HIT_SKIP } else { return@castRayNaive RayFilterResult.SKIP } } return result } /** * Трассирует лучи света вокруг себя с заданной позицией, интенсивностью, * падением интенсивности за проход сквозь тайл [falloffByTile] и * падением интенсивности за проход по пустому месту [falloffByTravel]. */ fun ICellAccess.rayLightCircleNaive( position: Vector2d, intensity: Double, falloffByTile: Double = 2.0, falloffByTravel: Double = 1.0, ): Double2DArray { val combinedResult = Double2DArray.allocate(intensity.roundToInt() * 2, intensity.roundToInt() * 2) val baselineX = position.x.roundToInt() - intensity.roundToInt() val baselineY = position.y.roundToInt() - intensity.roundToInt() val dirs = chooseLightRayFan(intensity) val mul = 1.0 / dirs.size for (dir in dirs) { val result2 = rayLightNaive(position, dir, intensity, falloffByTile, falloffByTravel) for (pair in result2) { combinedResult[pair.second.y - baselineY, pair.second.x - baselineX] += pair.first * mul } } return combinedResult }