174 lines
4.8 KiB
Kotlin
174 lines
4.8 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
|
|
|
|
object Never : TileRayFilter {
|
|
override fun test(
|
|
cell: AbstractCell,
|
|
fraction: Double,
|
|
x: Int,
|
|
y: Int,
|
|
normal: RayDirection,
|
|
borderX: Double,
|
|
borderY: Double
|
|
): RayFilterResult {
|
|
return RayFilterResult.CONTINUE
|
|
}
|
|
}
|
|
|
|
object Solid : TileRayFilter {
|
|
override fun test(
|
|
cell: AbstractCell,
|
|
fraction: Double,
|
|
x: Int,
|
|
y: Int,
|
|
normal: RayDirection,
|
|
borderX: Double,
|
|
borderY: Double
|
|
): RayFilterResult {
|
|
return RayFilterResult.of(cell.foreground.material.value.collisionKind.isSolidCollision)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|