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