Actual Light test

This commit is contained in:
DBotThePony 2023-09-08 00:00:43 +07:00
parent 0f4b7ace07
commit 6397637538
Signed by: DBot
GPG Key ID: DCC23B5715498507
7 changed files with 732 additions and 139 deletions

View File

@ -4,6 +4,8 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.longs.LongArraySet import it.unimi.dsi.fastutil.longs.LongArraySet
import it.unimi.dsi.fastutil.objects.ReferenceArraySet 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.ConfiguredMesh
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.client.render.Mesh 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.RGBAColor
import ru.dbotthepony.kvector.vector.Vector2f import ru.dbotthepony.kvector.vector.Vector2f
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
import kotlin.math.roundToInt
class ClientWorld( class ClientWorld(
val client: StarboundClient, 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 lightsize = 16
val lightmap = floodLight( val lightmap = floodLight(
Vector2i(pos.x.roundToInt(), pos.y.roundToInt()), lightsize Vector2i(pos.x.roundToInt(), pos.y.roundToInt()), lightsize
) )
client.gl.quadWireframe { client.gl.quadColor {
for (column in 0 until lightmap.columns) { for (column in 0 until lightmap.columns) {
for (row in 0 until lightmap.rows) { for (row in 0 until lightmap.rows) {
if (lightmap[column, row] > 0) { 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),
)
} }
} }
} }

View File

@ -12,14 +12,20 @@ import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.gl.BlendFunc import ru.dbotthepony.kstarbound.client.gl.BlendFunc
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker 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.input.UserInput
import ru.dbotthepony.kstarbound.client.render.Camera import ru.dbotthepony.kstarbound.client.render.Camera
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.client.render.TextAlignY import ru.dbotthepony.kstarbound.client.render.TextAlignY
import ru.dbotthepony.kstarbound.client.render.TileRenderers 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.JVMTimeSource
import ru.dbotthepony.kstarbound.util.PausableTimeSource import ru.dbotthepony.kstarbound.util.PausableTimeSource
import ru.dbotthepony.kstarbound.util.formatBytesShort 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.arrays.Matrix4f
import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.RGBAColor
@ -30,9 +36,9 @@ import ru.dbotthepony.kvector.vector.Vector3f
import java.io.Closeable import java.io.Closeable
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
import java.util.*
import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.LockSupport
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.math.roundToInt
class StarboundClient(val starbound: Starbound) : Closeable { class StarboundClient(val starbound: Starbound) : Closeable {
val time = PausableTimeSource(JVMTimeSource.INSTANCE) val time = PausableTimeSource(JVMTimeSource.INSTANCE)
@ -260,6 +266,46 @@ class StarboundClient(val starbound: Starbound) : Closeable {
val settings = ClientSettings() 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 onDrawGUI = ArrayList<() -> Unit>()
private val onPreDrawWorld = ArrayList<(LayeredRenderer) -> Unit>() private val onPreDrawWorld = ArrayList<(LayeredRenderer) -> Unit>()
private val onPostDrawWorld = ArrayList<() -> Unit>() private val onPostDrawWorld = ArrayList<() -> Unit>()
@ -305,6 +351,7 @@ class StarboundClient(val starbound: Starbound) : Closeable {
val world = world val world = world
if (world != null) { if (world != null) {
updateViewportParams()
val layers = LayeredRenderer() val layers = LayeredRenderer()
if (frameRenderTime != 0.0 && starbound.initialized) if (frameRenderTime != 0.0 && starbound.initialized)
@ -330,13 +377,50 @@ class StarboundClient(val starbound: Starbound) : Closeable {
world.addLayers( world.addLayers(
layers = layers, layers = layers,
size = AABB.rectangle( size = viewportRectangle)
camera.pos.toDoubleVector(),
viewportWidth / settings.zoom / PIXELS_IN_STARBOUND_UNIT,
viewportHeight / settings.zoom / PIXELS_IN_STARBOUND_UNIT))
layers.render(gl.matrixStack) 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() world.physics.debugDraw()
for (lambda in onPostDrawWorld) { for (lambda in onPostDrawWorld) {

View File

@ -1,5 +1,7 @@
package ru.dbotthepony.kstarbound.client.gl.vertex package ru.dbotthepony.kstarbound.client.gl.vertex
import ru.dbotthepony.kvector.vector.RGBAColor
typealias QuadVertexTransformer = (VertexBuilder, Int) -> VertexBuilder typealias QuadVertexTransformer = (VertexBuilder, Int) -> VertexBuilder
val EMPTY_VERTEX_TRANSFORM: QuadVertexTransformer = { it, _ -> it } 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 -> return transformer@{ it, index ->
when (index) { it.pushVec4f(x, y, z, w)
0 -> it.pushVec2f(0f, 0f) return@transformer after(it, index)
1 -> it.pushVec2f(1f, 0f)
2 -> it.pushVec2f(0f, 1f)
3 -> it.pushVec2f(1f, 1f)
}
return@transformer lambda(it, index)
} }
} }
} }

View File

@ -37,11 +37,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
var changeset = 0 var changeset = 0
private set(value) { private set(value) {
field = value field = value
promote()
if (isEmpty) {
isEmpty = false
world.chunkMap.promote(this as This)
}
} }
var tileChangeset = 0 var tileChangeset = 0
@ -60,21 +56,32 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
private var isEmpty = true private var isEmpty = true
protected val cells by lazy { fun promote() {
if (isEmpty) {
isEmpty = false
world.chunkMap.promote(this as This)
}
}
protected val cells = lazy {
Object2DArray.nulls<Cell>(CHUNK_SIZE, CHUNK_SIZE) Object2DArray.nulls<Cell>(CHUNK_SIZE, CHUNK_SIZE)
} }
override fun getCell(x: Int, y: Int): IChunkCell { override fun getCell(x: Int, y: Int): IChunkCell {
var get = cells[x, y] var get = cells.value[x, y]
if (get == null) { if (get == null) {
get = Cell(x, y) get = Cell(x, y)
cells[x, y] = get cells.value[x, y] = get
} }
return get return get
} }
override fun getCellDirect(x: Int, y: Int): IChunkCell {
return getCell(x, y)
}
// local cells' tile access // local cells' tile access
val localBackgroundView = TileView.Background(this) val localBackgroundView = TileView.Background(this)
val localForegroundView = TileView.Foreground(this) val localForegroundView = TileView.Foreground(this)

View File

@ -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<LightCell>(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<LightCell>(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<Cell>(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<PointLight>()
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<TaskCluster>(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<PointLight>()
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<TaskCluster>()
private val threads = ArrayList<Thread>()
private val clusterColors = ArrayList<RGBAColor>()
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))
}
}
}
}

View File

@ -418,107 +418,4 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
return false return false
} }
// Свет
private fun floodLightInto(
lightmap: Int2DArray,
thisIntensity: Int,
lightBlockerStrength: Int,
posX: Int,
worldPosX: Int,
posY: Int,
worldPosY: Int,
): Int {
if (lightmap[posX, posY] >= 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
}
} }

View File

@ -15,15 +15,7 @@ interface ICellAccess {
* non-null - valid cell and not wrapped around * non-null - valid cell and not wrapped around
* null - invalid cell (outside world bounds) * null - invalid cell (outside world bounds)
*/ */
fun getCellDirect(x: Int, y: Int): IChunkCell? { 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(pos: IStruct2i) = getCellDirect(pos.component1(), pos.component2()) fun getCellDirect(pos: IStruct2i) = getCellDirect(pos.component1(), pos.component2())
/** /**
@ -53,13 +45,17 @@ interface ICellAccess {
fun randomDoubleFor(pos: Vector2i) = randomDoubleFor(pos.x, pos.y) 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()) constructor(parent: ICellAccess, offset: IStruct2i) : this(parent, offset.component1(), offset.component2())
override fun getCell(x: Int, y: Int): IChunkCell? { override fun getCell(x: Int, y: Int): IChunkCell? {
return parent.getCell(x + this.x, y + this.y) 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 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 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) override fun randomLongFor(pos: Vector2i) = parent.randomLongFor(pos.x + this.x, pos.y + this.y)