KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt

230 lines
7.1 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}