230 lines
7.1 KiB
Kotlin
230 lines
7.1 KiB
Kotlin
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<Pair<Vector2i, IChunkCell>>,
|
||
val hitTile: Pair<Vector2i, IChunkCell>?,
|
||
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 .. 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<Pair<Vector2i, IChunkCell>>()
|
||
var prev = Vector2i(Int.MIN_VALUE, Int.MAX_VALUE)
|
||
var hitTile: Pair<Vector2i, IChunkCell>? = 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<Pair<Double, Vector2i>> {
|
||
val result = ArrayList<Pair<Double, Vector2i>>()
|
||
|
||
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
|
||
}
|