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

149 lines
4.5 KiB
Kotlin

package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.AbstractCell
import kotlin.collections.ArrayList
import kotlin.math.pow
import kotlin.math.sqrt
data class RayCastResult(
val traversedTiles: List<HitCell>,
val hitTile: HitCell?,
val fraction: Double,
val startPos: Vector2d,
val hitPos: Vector2d,
val direction: Vector2d
) {
constructor(startPos: Vector2d, direction: Vector2d) : this(listOf(), null, 0.0, startPos, startPos, direction)
data class HitCell(val pos: Vector2i, val normal: RayDirection, val borderCross: Vector2d, val cell: AbstractCell)
}
enum class RayFilterResult(val hit: Boolean, val write: Boolean) {
/**
* stop tracing, write hit tile into traversed tiles list
*/
HIT(true, true),
/**
* stop tracing, don't write hit tile into traversed tiles list
*/
BREAK(true, false),
/**
* continue tracing, don't write hit tile into traversed tiles list
*/
SKIP(false, false),
/**
* continue tracing, write hit tile into traversed tiles list
*/
CONTINUE(false, true);
companion object {
fun of(boolean: Boolean): RayFilterResult {
return if (boolean) HIT else SKIP
}
}
}
fun interface TileRayFilter {
/**
* [x] and [y] are wrapped around positions
*/
fun test(cell: AbstractCell, fraction: Double, x: Int, y: Int, normal: RayDirection, borderX: Double, borderY: Double): RayFilterResult
}
val NeverFilter = TileRayFilter { state, fraction, x, y, normal, borderX, borderY -> RayFilterResult.CONTINUE }
val NonEmptyFilter = TileRayFilter { state, fraction, x, y, normal, borderX, borderY -> RayFilterResult.of(!state.foreground.material.value.collisionKind.isEmpty) }
fun ICellAccess.castRay(startPos: Vector2d, direction: Vector2d, length: Double, filter: TileRayFilter) = castRay(startPos, startPos + direction * length, filter)
// https://www.youtube.com/watch?v=NbSee-XM7WA
fun ICellAccess.castRay(
start: Vector2d,
end: Vector2d,
filter: TileRayFilter
): RayCastResult {
if (start == end)
return RayCastResult(start, Vector2d.ZERO)
val hitTiles = ArrayList<RayCastResult.HitCell>()
var cellPosX = roundTowardsNegativeInfinity(start.x)
var cellPosY = roundTowardsNegativeInfinity(start.y)
var cell = getCell(cellPosX, cellPosY) ?: return RayCastResult(start, Vector2d.ZERO)
val direction = (end - start).unitVector
var result = filter.test(cell, 0.0, cellPosX, cellPosY, RayDirection.NONE, start.x, start.y)
if (result.write) hitTiles.add(RayCastResult.HitCell(Vector2i(cellPosX, cellPosY), RayDirection.NONE, start, cell))
if (result.hit) return RayCastResult(hitTiles, RayCastResult.HitCell(Vector2i(cellPosX, cellPosY), RayDirection.NONE, start, cell), 0.0, start, start, direction)
val distance = start.distance(end)
var travelled = 0.0
val unitStepSizeX = sqrt(1 + (direction.y / direction.x).pow(2.0))
val unitStepSizeY = sqrt(1 + (direction.x / direction.y).pow(2.0))
val stepX: Int
val stepY: Int
val xNormal: RayDirection
val yNormal: RayDirection
var rayLengthX: Double
var rayLengthY: Double
if (direction.x < 0.0) {
stepX = -1
rayLengthX = (start.x - cellPosX) * unitStepSizeX
xNormal = RayDirection.RIGHT
} else {
stepX = 1
rayLengthX = (cellPosX - start.x + 1) * unitStepSizeX
xNormal = RayDirection.LEFT
}
if (direction.y < 0.0) {
stepY = -1
rayLengthY = (start.y - cellPosY) * unitStepSizeY
yNormal = RayDirection.UP
} else {
stepY = 1
rayLengthY = (cellPosY - start.y + 1) * unitStepSizeY
yNormal = RayDirection.DOWN
}
while (travelled < distance) {
val normal: RayDirection
if (rayLengthX < rayLengthY) {
cellPosX += stepX
travelled = rayLengthX
rayLengthX += unitStepSizeX
normal = xNormal
} else {
cellPosY += stepY
travelled = rayLengthY
rayLengthY += unitStepSizeY
normal = yNormal
}
cell = getCell(cellPosX, cellPosY)
result = filter.test(cell, 0.0, cellPosX, cellPosY, normal, start.x + direction.x * travelled, start.y + direction.y * travelled)
val c = if (result.write || result.hit) {
RayCastResult.HitCell(Vector2i(cellPosX, cellPosY), normal, start + direction * travelled, cell)
} else {
null
}
if (result.write) hitTiles.add(c!!)
if (result.hit) return RayCastResult(hitTiles, c, travelled / distance, start, start + direction * travelled, direction)
}
return RayCastResult(hitTiles, null, 1.0, start, start + direction * distance, direction)
}