diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt index 14f18aa5..30e134f6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt @@ -4,6 +4,8 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.LongArraySet import it.unimi.dsi.fastutil.objects.ReferenceArraySet +import ru.dbotthepony.kstarbound.client.gl.vertex.QuadTransformers +import ru.dbotthepony.kstarbound.client.gl.vertex.QuadVertexTransformer import ru.dbotthepony.kstarbound.client.render.ConfiguredMesh import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.render.Mesh @@ -24,6 +26,7 @@ import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.Vector2f import ru.dbotthepony.kvector.vector.Vector2i +import kotlin.math.roundToInt class ClientWorld( val client: StarboundClient, @@ -261,20 +264,28 @@ class ClientWorld( } } - val pos = client.screenToWorld(client.mouseCoordinatesF).toDoubleVector() + /*val pos = client.screenToWorld(client.mouseCoordinatesF).toDoubleVector() - /*layers.add(-999999) { + layers.add(-999999) { val lightsize = 16 val lightmap = floodLight( Vector2i(pos.x.roundToInt(), pos.y.roundToInt()), lightsize ) - client.gl.quadWireframe { + client.gl.quadColor { for (column in 0 until lightmap.columns) { for (row in 0 until lightmap.rows) { if (lightmap[column, row] > 0) { - it.quad(pos.x.roundToInt() + column.toFloat() - lightsize, pos.y.roundToInt() + row.toFloat() - lightsize, pos.x.roundToInt() + column + 1f - lightsize, pos.y.roundToInt() + row + 1f - lightsize) + val color = lightmap[column, row] / 16f + + it.quad( + pos.x.roundToInt() + column.toFloat() - lightsize, + pos.y.roundToInt() + row.toFloat() - lightsize, + pos.x.roundToInt() + column + 1f - lightsize, + pos.y.roundToInt() + row + 1f - lightsize, + QuadTransformers.vec4(color, color, color, color), + ) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index 0d347088..32793d52 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -12,14 +12,20 @@ import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.gl.BlendFunc import ru.dbotthepony.kstarbound.client.gl.GLStateTracker +import ru.dbotthepony.kstarbound.client.gl.vertex.QuadTransformers import ru.dbotthepony.kstarbound.client.input.UserInput import ru.dbotthepony.kstarbound.client.render.Camera import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.render.TextAlignY import ru.dbotthepony.kstarbound.client.render.TileRenderers +import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity +import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity import ru.dbotthepony.kstarbound.util.JVMTimeSource import ru.dbotthepony.kstarbound.util.PausableTimeSource import ru.dbotthepony.kstarbound.util.formatBytesShort +import ru.dbotthepony.kstarbound.world.LightCalculator +import ru.dbotthepony.kstarbound.world.api.ICellAccess +import ru.dbotthepony.kstarbound.world.api.IChunkCell import ru.dbotthepony.kvector.arrays.Matrix4f import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.vector.RGBAColor @@ -30,9 +36,9 @@ import ru.dbotthepony.kvector.vector.Vector3f import java.io.Closeable import java.nio.ByteBuffer import java.nio.ByteOrder -import java.util.* import java.util.concurrent.locks.LockSupport import kotlin.collections.ArrayList +import kotlin.math.roundToInt class StarboundClient(val starbound: Starbound) : Closeable { val time = PausableTimeSource(JVMTimeSource.INSTANCE) @@ -260,6 +266,46 @@ class StarboundClient(val starbound: Starbound) : Closeable { val settings = ClientSettings() + var viewportCellX = 0 + private set + var viewportCellY = 0 + private set + var viewportCellWidth = 0 + private set + var viewportCellHeight = 0 + private set + var viewportRectangle = AABB.rectangle(Vector2d.ZERO, 0.0, 0.0) + private set + + val viewportCells: ICellAccess = object : ICellAccess { + override fun getCell(x: Int, y: Int): IChunkCell? { + return world?.getCell(x + viewportCellX, y + viewportCellY) + } + + override fun getCellDirect(x: Int, y: Int): IChunkCell? { + return world?.getCellDirect(x + viewportCellX, y + viewportCellY) + } + } + + var viewportLighting = LightCalculator(viewportCells, viewportCellWidth, viewportCellHeight) + private set + + fun updateViewportParams() { + viewportRectangle = AABB.rectangle( + camera.pos.toDoubleVector(), + viewportWidth / settings.zoom / PIXELS_IN_STARBOUND_UNIT, + viewportHeight / settings.zoom / PIXELS_IN_STARBOUND_UNIT) + + viewportCellX = roundTowardsNegativeInfinity(viewportRectangle.mins.x) - 4 + viewportCellY = roundTowardsNegativeInfinity(viewportRectangle.mins.y) - 4 + viewportCellWidth = roundTowardsPositiveInfinity(viewportRectangle.width) + 8 + viewportCellHeight = roundTowardsPositiveInfinity(viewportRectangle.height) + 8 + + if (viewportLighting.width != viewportCellWidth || viewportLighting.height != viewportCellHeight) { + viewportLighting = LightCalculator(viewportCells, viewportCellWidth, viewportCellHeight) + } + } + private val onDrawGUI = ArrayList<() -> Unit>() private val onPreDrawWorld = ArrayList<(LayeredRenderer) -> Unit>() private val onPostDrawWorld = ArrayList<() -> Unit>() @@ -305,6 +351,7 @@ class StarboundClient(val starbound: Starbound) : Closeable { val world = world if (world != null) { + updateViewportParams() val layers = LayeredRenderer() if (frameRenderTime != 0.0 && starbound.initialized) @@ -330,13 +377,50 @@ class StarboundClient(val starbound: Starbound) : Closeable { world.addLayers( layers = layers, - size = AABB.rectangle( - camera.pos.toDoubleVector(), - viewportWidth / settings.zoom / PIXELS_IN_STARBOUND_UNIT, - viewportHeight / settings.zoom / PIXELS_IN_STARBOUND_UNIT)) + size = viewportRectangle) layers.render(gl.matrixStack) + /*viewportLighting.clear() + + val (x, y) = screenToWorld(mouseCoordinates) + //viewportLighting.addPointLight(x.roundToInt() - viewportCellX, y.roundToInt() - viewportCellY, 179f / 255f, 149f / 255f, 107f / 255f) + val ix = x.roundToInt() + val iy = y.roundToInt() + + viewportLighting.addPointLight(ix - viewportCellX, iy - viewportCellY, 1f, 1f, 1f) + viewportLighting.addPointLight(ix - viewportCellX - 16, iy - viewportCellY - 4, 1f, 1f, 1f) + viewportLighting.addPointLight(ix - viewportCellX + 17, iy - viewportCellY - 20, 1f, 1f, 1f) + + viewportLighting.addPointLight(ix - viewportCellX + 19, iy - viewportCellY - 35, 1f, 1f, 1f) + viewportLighting.addPointLight(ix - viewportCellX + 21, iy - viewportCellY - 39, 1f, 1f, 1f) + viewportLighting.addPointLight(ix - viewportCellX + 24, iy - viewportCellY - 41, 1f, 1f, 1f) + + viewportLighting.addPointLight(ix - viewportCellX - 39, iy - viewportCellY - 35, 1f, 1f, 1f) + viewportLighting.addPointLight(ix - viewportCellX - 41, iy - viewportCellY - 39, 1f, 1f, 1f) + viewportLighting.addPointLight(ix - viewportCellX - 44, iy - viewportCellY - 41, 1f, 1f, 1f) + + viewportLighting.multithreaded = true + viewportLighting.calculate() + + gl.quadColor { + for (x in 0 until viewportLighting.width) { + for (y in 0 until viewportLighting.height) { + val cell = viewportLighting[x, y] + + if (cell.alpha > 0f) { + it.quad( + (viewportCellX + x).toFloat(), + (viewportCellY + y).toFloat(), + (viewportCellX + x + 1.0).toFloat(), + (viewportCellY + y + 1.0).toFloat(), + QuadTransformers.vec4(cell.red, cell.green, cell.blue, cell.alpha), + ) + } + } + } + }*/ + world.physics.debugDraw() for (lambda in onPostDrawWorld) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/QuadTransformers.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/QuadTransformers.kt index 7ab02db7..92175efc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/QuadTransformers.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/QuadTransformers.kt @@ -1,5 +1,7 @@ package ru.dbotthepony.kstarbound.client.gl.vertex +import ru.dbotthepony.kvector.vector.RGBAColor + typealias QuadVertexTransformer = (VertexBuilder, Int) -> VertexBuilder val EMPTY_VERTEX_TRANSFORM: QuadVertexTransformer = { it, _ -> it } @@ -50,16 +52,10 @@ object QuadTransformers { } } - fun uv(lambda: QuadVertexTransformer): QuadVertexTransformer { + fun vec4(x: Float, y: Float, z: Float, w: Float, after: QuadVertexTransformer = EMPTY_VERTEX_TRANSFORM): QuadVertexTransformer { return transformer@{ it, index -> - when (index) { - 0 -> it.pushVec2f(0f, 0f) - 1 -> it.pushVec2f(1f, 0f) - 2 -> it.pushVec2f(0f, 1f) - 3 -> it.pushVec2f(1f, 1f) - } - - return@transformer lambda(it, index) + it.pushVec4f(x, y, z, w) + return@transformer after(it, index) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index 35cd9e30..f823231d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -37,11 +37,7 @@ abstract class Chunk, This : Chunk, This : Chunk(CHUNK_SIZE, CHUNK_SIZE) } override fun getCell(x: Int, y: Int): IChunkCell { - var get = cells[x, y] + var get = cells.value[x, y] if (get == null) { get = Cell(x, y) - cells[x, y] = get + cells.value[x, y] = get } return get } + override fun getCellDirect(x: Int, y: Int): IChunkCell { + return getCell(x, y) + } + // local cells' tile access val localBackgroundView = TileView.Background(this) val localForegroundView = TileView.Foreground(this) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/LightCalculator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/LightCalculator.kt new file mode 100644 index 00000000..87108def --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/LightCalculator.kt @@ -0,0 +1,602 @@ +package ru.dbotthepony.kstarbound.world + +import it.unimi.dsi.fastutil.ints.IntArraySet +import ru.dbotthepony.kstarbound.world.api.ICellAccess +import ru.dbotthepony.kvector.api.IStruct4f +import ru.dbotthepony.kvector.arrays.Object2DArray +import ru.dbotthepony.kvector.util.linearInterpolation +import ru.dbotthepony.kvector.vector.RGBAColor +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.locks.LockSupport +import kotlin.math.roundToInt +import kotlin.random.Random + +// this implementation quite closely resembles original code, mostly +// because i found no other solution for light spreading +// however, code flow is HEAVILY altered, with many additions and changes +class LightCalculator(val parent: ICellAccess, val width: Int, val height: Int) { + enum class Quality(val secondDiagonal: Boolean, val extraCell: Boolean) { + HIGH(true, false), + MEDIUM(false, true), + LOW(false, false) + } + + interface ICell : IStruct4f { + val red: Float + val green: Float + val blue: Float + + val alpha: Float get() { + return (red * red + green * green + blue * blue).coerceIn(0f, 1f) + } + + override fun component1(): Float { + return red + } + + override fun component2(): Float { + return green + } + + override fun component3(): Float { + return blue + } + + override fun component4(): Float { + return alpha + } + } + + private fun interface Getter { + operator fun get(x: Int, y: Int): Grid.LightCell + } + + private inner class Grid { + inner class LightCell(val x: Int, val y: Int) : ICell { + val actualDropoff by lazy(LazyThreadSafetyMode.NONE) { + val parent = this@LightCalculator.parent.getCell(x, y) + val lightBlockStrength: Float + + if (parent?.foreground?.material != null) { + lightBlockStrength = 1f + } else { + lightBlockStrength = 0f + } + + linearInterpolation(lightBlockStrength, invMaxAirSpread, invMaxObstacleSpread) + } + + private var empty = true + override var red: Float = 0f + override var green: Float = 0f + override var blue: Float = 0f + + fun spreadInto(target: LightCell, drop: Float) { + val max = red.coerceAtLeast(green).coerceAtLeast(blue) + if (max <= 0f) return + val newDrop = 1f - actualDropoff / max * drop + + target.red = target.red.coerceAtLeast(red * newDrop) + target.green = target.green.coerceAtLeast(green * newDrop) + target.blue = target.blue.coerceAtLeast(blue * newDrop) + + if (target.empty && (target.red >= epsilon || target.blue >= epsilon || target.green >= epsilon)) { + minX = minX.coerceAtMost(target.x - 1) + minY = minY.coerceAtMost(target.y - 1) + maxX = maxX.coerceAtLeast(target.x + 1) + maxY = maxY.coerceAtLeast(target.y + 1) + target.empty = false + clampRect() + } + } + } + + var minX = width + var maxX = 0 + var minY = height + var maxY = 0 + + fun clampRect() { + minX = minX.coerceIn(1, width - 2) + maxX = maxX.coerceIn(1, width - 2) + minY = minY.coerceIn(1, height - 2) + maxY = maxY.coerceIn(1, height - 2) + } + + init { + clampRect() + } + + private val mem by lazy(LazyThreadSafetyMode.NONE) { + Object2DArray.nulls(width, height) + } + + operator fun get(x: Int, y: Int): LightCell { + var get = mem[x, y] + + if (get == null) { + get = LightCell(x, y) + mem[x, y] = get + } + + return get + } + + fun get0(x: Int, y: Int) = mem[x, y] + + fun safeGet(x: Int, y: Int): LightCell? { + if (x in 0 until width && y in 0 until height) { + return this[x, y] + } else { + return null + } + } + + /* + private val mem = Int2ObjectOpenHashMap(2048) + + operator fun get(x: Int, y: Int): LightCell { + var get = mem[x shl 16 or y] + + if (get == null) { + get = LightCell(x, y) + mem[x shl 16 or y] = get + } + + return get + } + + fun get0(x: Int, y: Int) = mem[x shl 16 or y] + + fun safeGet(x: Int, y: Int): LightCell? { + if (x in 0 until width && y in 0 until height) { + return this[x, y] + } else { + return null + } + }*/ + + private inner class Copy : Getter { + private val minX = this@Grid.minX + private val maxX = this@Grid.maxX + private val minY = this@Grid.minY + private val maxY = this@Grid.maxY + + private val mem = Object2DArray(maxX - minX + 3, maxY - minY + 3) { a, b -> this@Grid[a + minX - 1, b + minY - 1] } + + override fun get(x: Int, y: Int): LightCell { + return mem[x - minX + 1, y - minY + 1] + } + } + + private val passthrough = Getter { x, y -> this@Grid[x, y] } + + fun calculateSpread() { + if (minX > maxX || minY > maxY) return + + // spread light in several passes + var repeats = passes + + var copy: Getter? = null + + while (repeats-- >= 0) { + val minX = minX + val maxX = maxX + val minY = minY + val maxY = maxY + + if (copy == null) { + copy = if (maxX - minX >= width / 2 || maxY - minY >= height / 2) { + passthrough + } else { + Copy() + } + } + + // bottom to top + for (y in minY .. maxY) { + // left to right + for (x in minX .. maxX) { + val current = copy[x, y] + + current.spreadInto(copy[x, y + 1], 1f) + current.spreadInto(copy[x + 1, y], 1f) + current.spreadInto(copy[x + 1, y + 1], 1.4142135f) + + // original code performs this spread to camouflage prism shape of light spreading + // we instead gonna do light pass on different diagonal + if (quality.extraCell) + current.spreadInto(copy[x + 1, y - 1], 1.4142135f) + } + + // right to left + if (quality.secondDiagonal) { + for (x in maxX downTo minX) { + val current = copy[x, y] + + current.spreadInto(copy[x, y + 1], 1f) + current.spreadInto(copy[x - 1, y], 1f) + current.spreadInto(copy[x - 1, y + 1], 1.4142135f) + } + } + } + + // top to bottom + for (y in maxY downTo minY) { + // right to left + for (x in maxX downTo minX) { + val current = this[x, y] + + current.spreadInto(this[x, y - 1], 1f) + current.spreadInto(this[x - 1, y], 1f) + current.spreadInto(this[x - 1, y - 1], 1.4142135f) + + // original code performs this spread to camouflage prism shape of light spreading + // we instead gonna do light pass on different diagonal + if (quality.extraCell) + current.spreadInto(this[x - 1, y + 1], 1.4142135f) + } + + // left to right + if (quality.secondDiagonal) { + for (x in minX .. maxX) { + val current = this[x, y] + + current.spreadInto(this[x, y - 1], 1f) + current.spreadInto(this[x + 1, y], 1f) + current.spreadInto(this[x + 1, y - 1], 1.4142135f) + } + } + } + + // if our boundaries have updated, re-spread light + if ( + minX != this.minX || + maxX != this.maxX || + minY != this.minY || + maxY != this.maxY + ) { + repeats++ + + copy = if (this.maxX - this.minX >= width / 2 || this.maxY - this.minY >= height / 2) { + passthrough + } else { + Copy() + } + } + } + } + } + + var quality = Quality.HIGH + + // light values below this are considered too small to bother with + var epsilon = 0.01f + + // values below are specified by lighting.config + + // Number of ambient spread passes. Needs to be at least spreadMaxAir / + // spreadMaxObstacle big, but sometimes it can stand to be a bit less and + // you won't notice. + var passes = 3 + + // Maximum distance through empty space that 100% ambient light can pass through + var maxAirSpread = 32f + set(value) { + field = value + invMaxAirSpread = 1f / field + } + + // Maximum distance through rock that 100% ambient light can pass through + var maxObstacleSpread = 8f + set(value) { + field = value + invMaxObstacleSpread = 1f / field + } + + private var invMaxAirSpread = 1f / maxAirSpread + private var invMaxObstacleSpread = 1f / maxObstacleSpread + + // Maximum distance through emtpy space that 100% point light can pass through + var pointMaxAir = 48f + // Maximum distance through rock that 100% point light can pass through + var pointMaxObstacle = 9f + + private data class Cell(override var red: Float = 0f, override var green: Float = 0f, override var blue: Float = 0f) : ICell + + private var mainGrid = lazy { + Object2DArray.nulls(width, height) + } + + private object Empty : ICell { + override val red: Float = 0f + override val green: Float = 0f + override val blue: Float = 0f + override val alpha: Float = 0f + } + + private class PointLight(val x: Int, val y: Int, var red: Float, var green: Float, var blue: Float) { + var assignedTo: TaskCluster? = null + + fun error(to: TaskCluster): Double { + val dx = x - to.x + val dy = y - to.y + return dx * dx + dy * dy + } + } + + private val pointLights = ArrayList() + + fun addPointLight(x: Int, y: Int, red: Float, green: Float, blue: Float) { + if (x !in 0 until width || y !in 0 until height) return + pointLights.add(PointLight(x, y, red, green, blue)) + } + + operator fun get(x: Int, y: Int): ICell { + if (!mainGrid.isInitialized()) return Empty + return mainGrid.value[x, y] ?: Empty + } + + var multithreaded = false + + fun calculate() { + if (pointLights.isEmpty()) return + + // perform multithreaded calculation only when it makes sense + if (multithreaded && pointLights.size > 1) { + val mainGrid = mainGrid.value + val thread = Thread.currentThread() + + // calculate k-means clusters of point lights + // to effectively utilize CPU cores + val clusterCount = threads.size.coerceAtMost(pointLights.size) + val clusters = ArrayList(clusterCount) + val startingPoints = IntArraySet() + val rand = Random(System.nanoTime()) + + while (startingPoints.size < clusterCount) { + //startingPoints.add(rand.nextInt(0, pointLights.size)) + startingPoints.add(startingPoints.size) + } + + for (index in startingPoints.intIterator()) { + clusters.add(TaskCluster(pointLights[index].x.toDouble(), pointLights[index].y.toDouble(), this, thread)) + } + + var converged = false + + while (!converged) { + converged = true + + // assign + for (light in pointLights) { + val oldCluster = light.assignedTo + + // do selection sort since it is faster here + for (cluster in clusters) { + if (cluster === light.assignedTo) continue + val old = light.assignedTo?.let { light.error(it) } ?: Double.MAX_VALUE + val new = light.error(cluster) + + if (new < old) { + light.assignedTo = cluster + } + } + + if (oldCluster != light.assignedTo) { + oldCluster?.lights?.remove(light) + light.assignedTo!!.lights.add(light) + converged = false + } + } + + // update center of mass + for (cluster in clusters) { + cluster.updateCenter() + } + + // we settled on clusters + // check their centres of mass, and probably + // merge clusters which are too close to each other, + // to avoid excess work + // if we merge something, re-run k-clusters algorithm + if (converged) { + for (cluster1 in clusters) { + for (cluster2 in clusters) { + if (cluster1 === cluster2) continue + // don't create big clusters + if (cluster1.lights.size >= 4 && cluster2.lights.size >= 4) continue + + val dx = cluster1.x - cluster2.x + val dy = cluster1.y - cluster2.y + + val distance = dx * dx + dy * dy + + if (distance <= 64.0) { + cluster1.lights.addAll(cluster2.lights) + clusters.remove(cluster2) + converged = false + break + } + } + + if (!converged) break + } + } + } + + for ((i, cluster) in clusters.withIndex()) { + val (r, g, b) = clusterColors[i] + + for (light in cluster.lights) { + light.red = r + light.green = g + light.blue = b + } + } + + tasks.addAll(clusters) + wakeup() + + while (clusters.isNotEmpty()) { + clusters.removeIf { + val grid = it.grid + + if (grid != null) { + for (x in grid.minX - 1 .. grid.maxX) { + for (y in grid.minY - 1 .. grid.maxY) { + val a = grid.get0(x, y) ?: continue + var b = mainGrid[x, y] + + if (b == null) { + b = Cell() + mainGrid[x, y] = b + } + + b.red = a.red.coerceAtLeast(b.red) + b.green = a.green.coerceAtLeast(b.green) + b.blue = a.blue.coerceAtLeast(b.blue) + } + } + } + + grid != null + } + + LockSupport.parkNanos(500_000) + } + } else { + val grid = Grid() + val mainGrid = mainGrid.value + + for (light in pointLights) { + val cell = grid.safeGet(light.x, light.y) ?: continue + val speculatedSpread = (maxAirSpread * light.red.coerceAtLeast(light.green).coerceAtLeast(light.blue)).roundToInt() + + cell.red = light.red + cell.green = light.green + cell.blue = light.blue + + grid.minX = grid.minX.coerceAtMost(light.x - speculatedSpread) + grid.minY = grid.minY.coerceAtMost(light.y - speculatedSpread) + + grid.maxX = grid.maxX.coerceAtLeast(light.x + speculatedSpread) + grid.maxY = grid.maxY.coerceAtLeast(light.y + speculatedSpread) + + grid.clampRect() + } + + grid.calculateSpread() + + for (x in grid.minX - 1 .. grid.maxX) { + for (y in grid.minY - 1 .. grid.maxY) { + val a = grid.get0(x, y) ?: continue + + mainGrid[x, y] = Cell( + a.red, + a.green, + a.blue, + ) + } + } + } + } + + fun clear() { + pointLights.clear() + mainGrid = lazy { Object2DArray.nulls(width, height) } + } + + private class TaskCluster( + var x: Double, + var y: Double, + val parent: LightCalculator, + val parentThread: Thread + ) { + val lights = ArrayList() + + fun updateCenter() { + if (lights.isEmpty()) return + + x = lights.first().x.toDouble() + y = lights.first().y.toDouble() + + for (i in 1 until lights.size) { + x += lights[i].x.toDouble() + y += lights[i].y.toDouble() + } + + x /= lights.size + y /= lights.size + } + + @Volatile + var grid: Grid? = null + private set + + fun execute() { + if (grid != null) return + + val grid = parent.Grid() + + for (light in lights) { + val speculatedSpread = (parent.maxAirSpread * light.red.coerceAtLeast(light.green).coerceAtLeast(light.blue)).roundToInt() + val cell = grid.safeGet(light.x, light.y) ?: continue + + cell.red = light.red + cell.green = light.green + cell.blue = light.blue + + grid.minX = grid.minX.coerceAtMost(light.x - speculatedSpread) + grid.minY = grid.minY.coerceAtMost(light.y - speculatedSpread) + + grid.maxX = grid.maxX.coerceAtLeast(light.x + speculatedSpread) + grid.maxY = grid.maxY.coerceAtLeast(light.y + speculatedSpread) + + grid.clampRect() + } + + grid.calculateSpread() + this.grid = grid + LockSupport.unpark(parentThread) + } + } + + companion object { + private val tasks = ConcurrentLinkedQueue() + private val threads = ArrayList() + + private val clusterColors = ArrayList() + + private fun wakeup() { + for (thread in threads) { + LockSupport.unpark(thread) + } + } + + private fun thread() { + while (true) { + val next = tasks.poll() + + if (next == null) { + LockSupport.park() + continue + } + + next.execute() + } + } + + init { + for (i in 0 until Runtime.getRuntime().availableProcessors()) { + Thread(::thread, "Starbound Lighting Thread $i").also { threads.add(it); it.isDaemon = true }.start() + } + + val rand = Random(System.nanoTime()) + + for (i in threads.indices) { + clusterColors.add(RGBAColor(rand.nextFloat() * 0.5f + 0.5f, rand.nextFloat() * 0.5f + 0.5f, rand.nextFloat() * 0.5f + 0.5f)) + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 74259374..10c3bf7e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -418,107 +418,4 @@ abstract class World, ChunkType : Chunk= thisIntensity) { - return 1 - } - - val tile = getCell(worldPosX, worldPosY) - - var newIntensity: Int - - if (tile?.foreground?.material?.renderParameters?.lightTransparent == false) { - newIntensity = thisIntensity - lightBlockerStrength - } else { - newIntensity = thisIntensity - 1 - } - - if (tile?.foreground?.material != null) - newIntensity = 0 - - lightmap[posX, posY] = newIntensity.coerceAtLeast(0) - - if (newIntensity > 1) { - var c = 1 - - c += floodLightInto( - lightmap, newIntensity, lightBlockerStrength, - posX + 1, - worldPosX + 1, - posY, - worldPosY, - ) - - c += floodLightInto( - lightmap, newIntensity, lightBlockerStrength, - posX - 1, - worldPosX - 1, - posY, - worldPosY, - ) - - c += floodLightInto( - lightmap, newIntensity, lightBlockerStrength, - posX, - worldPosX, - posY + 1, - worldPosY + 1, - ) - - c += floodLightInto( - lightmap, newIntensity, lightBlockerStrength, - posX, - worldPosX, - posY - 1, - worldPosY - 1, - ) - - return c - } - - return 1 - } - - /** - * Просчитывает распространение света во все стороны на указанной позиции (в тайлах) - * - * [lightIntensity] - максимальное расстояние, которое может пройти свет из точки своего появления. - * Имеет жёсткое ограничение в [CHUNK_SIZE]. - * - * [lightBlockerStrength] - какова стоимость "пробития" тайла насквозь, который не пропускает свет - */ - fun floodLight( - lightPosition: Vector2i, - lightIntensity: Int, - lightBlockerStrength: Int = 4, - ): Int2DArray { - require(lightIntensity >= 1) { "Invalid light intensity $lightIntensity" } - require(lightBlockerStrength >= 1) { "Invalid light blocker strength $lightBlockerStrength" } - require(lightIntensity <= CHUNK_SIZE) { "Too intensive light! $lightIntensity" } - - val lightmap = Int2DArray.allocate(lightIntensity * 2 + 1, lightIntensity * 2 + 1) - - floodLightInto( - lightmap, - lightIntensity, - lightBlockerStrength, - lightIntensity, - lightPosition.x, - lightIntensity, - lightPosition.y, - ) - - return lightmap - } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ICellAccess.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ICellAccess.kt index a3691643..f0dfd5d3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ICellAccess.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ICellAccess.kt @@ -15,15 +15,7 @@ interface ICellAccess { * non-null - valid cell and not wrapped around * null - invalid cell (outside world bounds) */ - fun getCellDirect(x: Int, y: Int): IChunkCell? { - val cell = getCell(x, y) - - if (cell == null || cell.x != x || cell.y != y) - return null - - return cell - } - + fun getCellDirect(x: Int, y: Int): IChunkCell? fun getCellDirect(pos: IStruct2i) = getCellDirect(pos.component1(), pos.component2()) /** @@ -53,13 +45,17 @@ interface ICellAccess { fun randomDoubleFor(pos: Vector2i) = randomDoubleFor(pos.x, pos.y) } -class OffsetCellAccess(private val parent: ICellAccess, private val x: Int, private val y: Int) : ICellAccess { +class OffsetCellAccess(private val parent: ICellAccess, var x: Int, var y: Int) : ICellAccess { constructor(parent: ICellAccess, offset: IStruct2i) : this(parent, offset.component1(), offset.component2()) override fun getCell(x: Int, y: Int): IChunkCell? { return parent.getCell(x + this.x, y + this.y) } + override fun getCellDirect(x: Int, y: Int): IChunkCell? { + return parent.getCellDirect(x + this.x, y + this.y) + } + override fun randomLongFor(x: Int, y: Int) = parent.randomLongFor(x + this.x, y + this.y) override fun randomDoubleFor(x: Int, y: Int) = parent.randomDoubleFor(x + this.x, y + this.y) override fun randomLongFor(pos: Vector2i) = parent.randomLongFor(pos.x + this.x, pos.y + this.y)