Raycasted lights
This commit is contained in:
parent
96068d483c
commit
5947252dc7
@ -15,9 +15,13 @@ import ru.dbotthepony.kstarbound.world.*
|
||||
import ru.dbotthepony.kstarbound.world.entities.Entity
|
||||
import ru.dbotthepony.kvector.util2d.AABB
|
||||
import ru.dbotthepony.kvector.vector.Color
|
||||
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
|
||||
import ru.dbotthepony.kvector.vector.nfloat.Vector2f
|
||||
import ru.dbotthepony.kvector.vector.nint.Vector2i
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sin
|
||||
|
||||
class ClientWorld(
|
||||
val client: StarboundClient,
|
||||
@ -146,9 +150,9 @@ class ClientWorld(
|
||||
|
||||
client.gl.blendFunc = old
|
||||
|
||||
val pos = client.screenToWorld(client.mouseCoordinatesF)
|
||||
val pos = client.screenToWorld(client.mouseCoordinatesF).toDoubleVector()
|
||||
|
||||
val lightsize = 16
|
||||
/*val lightsize = 16
|
||||
|
||||
val lightmap = floodLight(
|
||||
Vector2i(pos.x.roundToInt(), pos.y.roundToInt()), lightsize
|
||||
@ -162,8 +166,44 @@ class ClientWorld(
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
/*
|
||||
val rayFan = ArrayList<Vector2d>()
|
||||
|
||||
for (i in 0 .. 359) {
|
||||
rayFan.add(Vector2d(cos(i / 180.0 * PI), sin(i / 180.0 * PI)))
|
||||
}
|
||||
|
||||
for (ray in rayFan) {
|
||||
val trace = castRayNaive(pos, ray, 16.0)
|
||||
|
||||
client.gl.quadWireframe {
|
||||
for ((tpos, tile) in trace.traversedTiles) {
|
||||
if (tile.material != null)
|
||||
it.quad(
|
||||
tpos.x.toFloat(),
|
||||
tpos.y.toFloat(),
|
||||
tpos.x + 1f,
|
||||
tpos.y + 1f
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
client.gl.quadWireframe {
|
||||
for ((intensity, tpos) in rayLightCircleNaive(pos, 24.0, falloffByTravel = 1.5, falloffByTile = 4.0)) {
|
||||
it.quad(
|
||||
tpos.x.toFloat(),
|
||||
tpos.y.toFloat(),
|
||||
tpos.x + 1f,
|
||||
tpos.y + 1f
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
physics.debugDraw()
|
||||
|
||||
/*for (renderer in determineRenderers) {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package ru.dbotthepony.kstarbound.world
|
||||
|
||||
import com.google.common.collect.ImmutableList
|
||||
import it.unimi.dsi.fastutil.objects.Object2DoubleAVLTreeMap
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
|
||||
import ru.dbotthepony.kbox2d.api.ContactImpulse
|
||||
@ -10,7 +12,6 @@ import ru.dbotthepony.kbox2d.dynamics.B2Fixture
|
||||
import ru.dbotthepony.kbox2d.dynamics.B2World
|
||||
import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact
|
||||
import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT
|
||||
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
|
||||
import ru.dbotthepony.kstarbound.math.*
|
||||
import ru.dbotthepony.kstarbound.util.Timer
|
||||
import ru.dbotthepony.kstarbound.world.entities.CollisionResolution
|
||||
@ -21,7 +22,11 @@ import ru.dbotthepony.kvector.util2d.AABB
|
||||
import ru.dbotthepony.kvector.util2d.AABBi
|
||||
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
|
||||
import ru.dbotthepony.kvector.vector.nint.Vector2i
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sin
|
||||
|
||||
const val EARTH_FREEFALL_ACCELERATION = 9.8312 / METRES_IN_STARBOUND_UNIT
|
||||
|
||||
@ -34,6 +39,89 @@ data class WorldSweepResult(
|
||||
|
||||
private const val EPSILON = 0.00001
|
||||
|
||||
data class RayCastResult(
|
||||
val traversedTiles: List<Pair<Vector2i, ITileState>>,
|
||||
val hitTile: Pair<Vector2i, ITileState>?,
|
||||
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 .. 12.0 -> veryRoughDirFan
|
||||
in 12.0 .. 18.0 -> roughDirFan
|
||||
in 18.0 .. 24.0 -> dirFan
|
||||
in 24.0 .. 32.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: ITileState, fraction: Double, position: Vector2i): RayFilterResult
|
||||
}
|
||||
|
||||
/**
|
||||
* Считает все тайлы неблокирующими
|
||||
*/
|
||||
val AnythingRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.CONTINUE }
|
||||
|
||||
/**
|
||||
* Попадает по первому не-пустому тайлу
|
||||
*/
|
||||
val NonSolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.material != null) }
|
||||
|
||||
/**
|
||||
* Попадает по первому пустому тайлу
|
||||
*/
|
||||
val SolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.material == null) }
|
||||
|
||||
/**
|
||||
* Попадает по первому тайлу который блокирует проход света
|
||||
*/
|
||||
val LineOfSightRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.material?.renderParameters?.lightTransparent == false) }
|
||||
|
||||
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(
|
||||
val seed: Long,
|
||||
val widthInChunks: Int
|
||||
@ -58,9 +146,6 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
*/
|
||||
val dirtyPhysicsChunks = HashSet<ChunkType>()
|
||||
|
||||
protected var lastAccessedChunk: ChunkType? = null
|
||||
protected var lastAccessedChunkPos: ChunkPos? = null
|
||||
|
||||
val physics = B2World(Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION))
|
||||
|
||||
private var timers = ArrayList<Timer>()
|
||||
@ -230,25 +315,12 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
*/
|
||||
open operator fun get(pos: ChunkPos): ChunkType? {
|
||||
if (!isCircular) {
|
||||
//if (lastAccessedChunkPos == pos) {
|
||||
// return lastAccessedChunk
|
||||
//}
|
||||
|
||||
//lastAccessedChunkPos = pos
|
||||
lastAccessedChunk = chunkMap[pos]
|
||||
return this.lastAccessedChunk
|
||||
return chunkMap[pos]
|
||||
}
|
||||
|
||||
@Suppress("Name_Shadowing")
|
||||
val pos = pos.circular(widthInChunks)
|
||||
|
||||
if (lastAccessedChunkPos == pos) {
|
||||
return lastAccessedChunk
|
||||
}
|
||||
|
||||
val load = chunkMap[pos]
|
||||
this.lastAccessedChunk = load
|
||||
return load
|
||||
return chunkMap[pos]
|
||||
}
|
||||
|
||||
open fun getInstantTuple(pos: ChunkPos): IWorldChunkTuple<This, ChunkType>? {
|
||||
@ -262,18 +334,9 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
@Suppress("Name_Shadowing")
|
||||
val pos = if (isCircular) pos.circular(widthInChunks) else pos
|
||||
|
||||
val _lastAccessedChunk = lastAccessedChunk
|
||||
|
||||
if (_lastAccessedChunk?.pos == pos) {
|
||||
return _lastAccessedChunk
|
||||
}
|
||||
|
||||
return chunkMap.computeIfAbsent(pos, Object2ObjectFunction {
|
||||
val chunk = chunkFactory(pos)
|
||||
|
||||
lastAccessedChunk = chunk
|
||||
lastAccessedChunkPos = pos
|
||||
|
||||
val orphanedInThisChunk = ArrayList<Entity>()
|
||||
|
||||
for (ent in orphanedEntities) {
|
||||
@ -292,6 +355,215 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Позволяет получать чанки/тайлы с минимальным кешем. Если один чанк считывается очень большое число раз,
|
||||
* то использование этого класса сильно ускорит работу.
|
||||
*
|
||||
* Так же реализует raycasting методы.
|
||||
*/
|
||||
inner class CachedGetter {
|
||||
private var lastChunk: ChunkType? = null
|
||||
private var lastPos: ChunkPos? = null
|
||||
|
||||
operator fun get(pos: ChunkPos): ChunkType? {
|
||||
if (lastPos == pos) {
|
||||
return lastChunk
|
||||
}
|
||||
|
||||
lastChunk = this@World[pos]
|
||||
lastPos = pos
|
||||
return lastChunk
|
||||
}
|
||||
|
||||
fun getTile(pos: Vector2i): ITileState? {
|
||||
return get(ChunkPos.fromTilePosition(pos))?.foreground?.get(ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y))
|
||||
}
|
||||
|
||||
fun getBackgroundTile(pos: Vector2i): ITileState? {
|
||||
return get(ChunkPos.fromTilePosition(pos))?.background?.get(ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y))
|
||||
}
|
||||
|
||||
/**
|
||||
* Бросает луч напротив тайлов мира с заданными позициями и фильтром
|
||||
*/
|
||||
fun 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 = ArrayList<Pair<Vector2i, ITileState>>()
|
||||
var prev = Vector2i(Int.MIN_VALUE, Int.MAX_VALUE)
|
||||
var hitTile: Pair<Vector2i, ITileState>? = null
|
||||
|
||||
while (t < 1.0) {
|
||||
val (x, y) = rayStart + dir * t
|
||||
val tilePos = Vector2i(x.roundToInt(), y.roundToInt())
|
||||
|
||||
if (tilePos != prev) {
|
||||
val tile = getTile(tilePos) ?: EmptyTileState
|
||||
|
||||
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 castRayNaive(
|
||||
rayPosition: Vector2d,
|
||||
direction: Vector2d,
|
||||
length: Double,
|
||||
filter: TileRayFilter = AnythingRayFilter
|
||||
): RayCastResult {
|
||||
return castRayNaive(rayPosition, rayPosition + direction.normalized * length, filter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Выпускает луч света с заданной силой (определяет длину луча и способность проходить сквозь тайлы), позицией и направлением.
|
||||
*
|
||||
* Позволяет указать отдельно [falloffByTile] потерю силы света при прохождении через тайлы.
|
||||
*/
|
||||
fun 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.material?.renderParameters?.lightTransparent == false) {
|
||||
currentIntensity -= falloffByTile
|
||||
} else {
|
||||
currentIntensity -= falloffByTravel
|
||||
}
|
||||
|
||||
result.add(currentIntensity to pos)
|
||||
|
||||
if (currentIntensity <= 0.0) {
|
||||
return@castRayNaive RayFilterResult.HIT
|
||||
} else {
|
||||
return@castRayNaive RayFilterResult.CONTINUE
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun rayLightCircleNaive(
|
||||
position: Vector2d,
|
||||
intensity: Double,
|
||||
falloffByTile: Double = 2.0,
|
||||
falloffByTravel: Double = 2.0,
|
||||
): List<Pair<Double, Vector2i>> {
|
||||
val result = Object2DoubleAVLTreeMap<Vector2i> { a, b ->
|
||||
val cmp = a.x.compareTo(b.x)
|
||||
|
||||
if (cmp != 0) {
|
||||
return@Object2DoubleAVLTreeMap cmp
|
||||
}
|
||||
|
||||
return@Object2DoubleAVLTreeMap a.y.compareTo(b.y)
|
||||
}
|
||||
|
||||
result.defaultReturnValue(-1.0)
|
||||
|
||||
for (dir in chooseLightRayFan(intensity)) {
|
||||
val result2 = rayLightNaive(position, dir, intensity, falloffByTile, falloffByTravel)
|
||||
|
||||
for (pair in result2) {
|
||||
val existing = result.getDouble(pair.second)
|
||||
|
||||
if (existing < pair.first) {
|
||||
result.put(pair.second, pair.first)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.map { it.value to it.key }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see CachedGetter.castRayNaive
|
||||
*/
|
||||
fun castRayNaive(
|
||||
rayStart: Vector2d,
|
||||
rayEnd: Vector2d,
|
||||
filter: TileRayFilter = AnythingRayFilter
|
||||
): RayCastResult {
|
||||
return CachedGetter().castRayNaive(rayStart, rayEnd, filter)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see CachedGetter.castRayNaive
|
||||
*/
|
||||
fun castRayNaive(
|
||||
rayPosition: Vector2d,
|
||||
direction: Vector2d,
|
||||
length: Double,
|
||||
filter: TileRayFilter = AnythingRayFilter
|
||||
): RayCastResult {
|
||||
return CachedGetter().castRayNaive(rayPosition, direction, length, filter)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see CachedGetter.rayLightNaive
|
||||
*/
|
||||
fun rayLightNaive(
|
||||
position: Vector2d,
|
||||
direction: Vector2d,
|
||||
intensity: Double,
|
||||
falloffByTile: Double = 2.0,
|
||||
falloffByTravel: Double = 1.0,
|
||||
): List<Pair<Double, Vector2i>> {
|
||||
return CachedGetter().rayLightNaive(position, direction, intensity, falloffByTile, falloffByTravel)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see CachedGetter.rayLightCircleNaive
|
||||
*/
|
||||
fun rayLightCircleNaive(
|
||||
position: Vector2d,
|
||||
intensity: Double,
|
||||
falloffByTile: Double = 2.0,
|
||||
falloffByTravel: Double = 2.0,
|
||||
): List<Pair<Double, Vector2i>> {
|
||||
return CachedGetter().rayLightCircleNaive(position, intensity, falloffByTile, falloffByTravel)
|
||||
}
|
||||
|
||||
open fun getForegroundView(pos: ChunkPos): TileView {
|
||||
val tuple = get(pos)?.let { InstantWorldChunkTuple(this as This, it) }
|
||||
|
||||
@ -661,7 +933,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
|
||||
val view = getRigidForegroundView(ChunkPos.fromTilePosition(lightPosition))
|
||||
|
||||
val calls = floodLightInto(
|
||||
floodLightInto(
|
||||
lightmap,
|
||||
view,
|
||||
lightIntensity,
|
||||
@ -672,8 +944,6 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
lightPosition.y - view.pos.tileY,
|
||||
)
|
||||
|
||||
println(calls)
|
||||
|
||||
return lightmap
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user