From 5947252dc78490b7020d844738a85bcf527f21f1 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Fri, 16 Sep 2022 17:47:34 +0700 Subject: [PATCH] Raycasted lights --- .../kstarbound/client/ClientWorld.kt | 44 ++- .../ru/dbotthepony/kstarbound/world/World.kt | 332 ++++++++++++++++-- 2 files changed, 343 insertions(+), 33 deletions(-) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt index deb45ac3..516755f2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt @@ -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() + + 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) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index bb7ddaef..5ab2c03f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -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>, + val hitTile: Pair?, + val fraction: Double +) + +private fun makeDirFan(step: Double): List { + var i = 0.0 + val result = ImmutableList.builder() + + 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 { + 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, ChunkType : Chunk>( val seed: Long, val widthInChunks: Int @@ -58,9 +146,6 @@ abstract class World, ChunkType : Chunk() - protected var lastAccessedChunk: ChunkType? = null - protected var lastAccessedChunkPos: ChunkPos? = null - val physics = B2World(Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION)) private var timers = ArrayList() @@ -230,25 +315,12 @@ abstract class World, ChunkType : Chunk? { @@ -262,18 +334,9 @@ abstract class World, ChunkType : Chunk() for (ent in orphanedEntities) { @@ -292,6 +355,215 @@ abstract class World, ChunkType : Chunk>() + var prev = Vector2i(Int.MIN_VALUE, Int.MAX_VALUE) + var hitTile: Pair? = 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> { + val result = ArrayList>() + + 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> { + val result = Object2DoubleAVLTreeMap { 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> { + 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> { + 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, ChunkType : Chunk, ChunkType : Chunk