Raycasted lights

This commit is contained in:
DBotThePony 2022-09-16 17:47:34 +07:00
parent 96068d483c
commit 5947252dc7
Signed by: DBot
GPG Key ID: DCC23B5715498507
2 changed files with 343 additions and 33 deletions

View File

@ -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) {

View File

@ -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
}
}