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.kstarbound.world.entities.Entity
import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.Color 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.nfloat.Vector2f
import ru.dbotthepony.kvector.vector.nint.Vector2i import ru.dbotthepony.kvector.vector.nint.Vector2i
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.sin
class ClientWorld( class ClientWorld(
val client: StarboundClient, val client: StarboundClient,
@ -146,9 +150,9 @@ class ClientWorld(
client.gl.blendFunc = old 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( val lightmap = floodLight(
Vector2i(pos.x.roundToInt(), pos.y.roundToInt()), lightsize 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() physics.debugDraw()
/*for (renderer in determineRenderers) { /*for (renderer in determineRenderers) {

View File

@ -1,5 +1,7 @@
package ru.dbotthepony.kstarbound.world 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.Object2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import ru.dbotthepony.kbox2d.api.ContactImpulse 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.B2World
import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact
import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.util.Timer import ru.dbotthepony.kstarbound.util.Timer
import ru.dbotthepony.kstarbound.world.entities.CollisionResolution 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.util2d.AABBi
import ru.dbotthepony.kvector.vector.ndouble.Vector2d import ru.dbotthepony.kvector.vector.ndouble.Vector2d
import ru.dbotthepony.kvector.vector.nint.Vector2i import ru.dbotthepony.kvector.vector.nint.Vector2i
import kotlin.math.PI
import kotlin.math.absoluteValue 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 const val EARTH_FREEFALL_ACCELERATION = 9.8312 / METRES_IN_STARBOUND_UNIT
@ -34,6 +39,89 @@ data class WorldSweepResult(
private const val EPSILON = 0.00001 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>>( abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(
val seed: Long, val seed: Long,
val widthInChunks: Int val widthInChunks: Int
@ -58,9 +146,6 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
*/ */
val dirtyPhysicsChunks = HashSet<ChunkType>() val dirtyPhysicsChunks = HashSet<ChunkType>()
protected var lastAccessedChunk: ChunkType? = null
protected var lastAccessedChunkPos: ChunkPos? = null
val physics = B2World(Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION)) val physics = B2World(Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION))
private var timers = ArrayList<Timer>() 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? { open operator fun get(pos: ChunkPos): ChunkType? {
if (!isCircular) { if (!isCircular) {
//if (lastAccessedChunkPos == pos) { return chunkMap[pos]
// return lastAccessedChunk
//}
//lastAccessedChunkPos = pos
lastAccessedChunk = chunkMap[pos]
return this.lastAccessedChunk
} }
@Suppress("Name_Shadowing") @Suppress("Name_Shadowing")
val pos = pos.circular(widthInChunks) val pos = pos.circular(widthInChunks)
return chunkMap[pos]
if (lastAccessedChunkPos == pos) {
return lastAccessedChunk
}
val load = chunkMap[pos]
this.lastAccessedChunk = load
return load
} }
open fun getInstantTuple(pos: ChunkPos): IWorldChunkTuple<This, ChunkType>? { 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") @Suppress("Name_Shadowing")
val pos = if (isCircular) pos.circular(widthInChunks) else pos val pos = if (isCircular) pos.circular(widthInChunks) else pos
val _lastAccessedChunk = lastAccessedChunk
if (_lastAccessedChunk?.pos == pos) {
return _lastAccessedChunk
}
return chunkMap.computeIfAbsent(pos, Object2ObjectFunction { return chunkMap.computeIfAbsent(pos, Object2ObjectFunction {
val chunk = chunkFactory(pos) val chunk = chunkFactory(pos)
lastAccessedChunk = chunk
lastAccessedChunkPos = pos
val orphanedInThisChunk = ArrayList<Entity>() val orphanedInThisChunk = ArrayList<Entity>()
for (ent in orphanedEntities) { 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 { open fun getForegroundView(pos: ChunkPos): TileView {
val tuple = get(pos)?.let { InstantWorldChunkTuple(this as This, it) } 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 view = getRigidForegroundView(ChunkPos.fromTilePosition(lightPosition))
val calls = floodLightInto( floodLightInto(
lightmap, lightmap,
view, view,
lightIntensity, lightIntensity,
@ -672,8 +944,6 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
lightPosition.y - view.pos.tileY, lightPosition.y - view.pos.tileY,
) )
println(calls)
return lightmap return lightmap
} }
} }