diff --git a/src/net/torvald/gdx/graphics/Cvec.kt b/src/net/torvald/gdx/graphics/Cvec.kt index adf48a53f..c346d66f2 100644 --- a/src/net/torvald/gdx/graphics/Cvec.kt +++ b/src/net/torvald/gdx/graphics/Cvec.kt @@ -70,6 +70,21 @@ class Cvec { set(color) } + /** + * Get RGBA Element using index, of which: + * - 0: R + * - 1: G + * - 2: B + * - 3: A + */ + fun getElem(index: Int) = when(index) { + 0 -> r + 1 -> g + 2 -> b + 3 -> a + else -> throw IndexOutOfBoundsException("Invalid index $index") + } + /** Sets this color to the given color. * * @param color the Cvec diff --git a/src/net/torvald/gdx/graphics/UnsafeCvecArray.kt b/src/net/torvald/gdx/graphics/UnsafeCvecArray.kt index f583548b8..fa11de090 100644 --- a/src/net/torvald/gdx/graphics/UnsafeCvecArray.kt +++ b/src/net/torvald/gdx/graphics/UnsafeCvecArray.kt @@ -29,6 +29,18 @@ internal class UnsafeCvecArray(val width: Int, val height: Int) { fun setB(x: Int, y: Int, value: Float) { array.setFloat(toAddr(x, y) + 8, value) } fun setA(x: Int, y: Int, value: Float) { array.setFloat(toAddr(x, y) + 12, value) } + /** + * @param channel 0 for R, 1 for G, 2 for B, 3 for A + */ + inline fun channelSet(x: Int, y: Int, channel: Int, value: Float) { + array.setFloat(toAddr(x, y) + 4L * channel, value) + } + + /** + * @param channel 0 for R, 1 for G, 2 for B, 3 for A + */ + inline fun channelGet(x: Int, y: Int, channel: Int) = array.getFloat(toAddr(x, y) + 4L * channel) + fun max(x: Int, y: Int, other: Cvec) { setR(x, y, maxOf(getR(x, y), other.r)) setG(x, y, maxOf(getG(x, y), other.g)) diff --git a/src/net/torvald/terrarum/blockproperties/BlockProp.kt b/src/net/torvald/terrarum/blockproperties/BlockProp.kt index 3f89e2258..34eabe93e 100644 --- a/src/net/torvald/terrarum/blockproperties/BlockProp.kt +++ b/src/net/torvald/terrarum/blockproperties/BlockProp.kt @@ -19,6 +19,14 @@ class BlockProp { lateinit var opacity: Cvec + fun getOpacity(channel: Int) = when (channel) { + 0 -> shadeColR + 1 -> shadeColG + 2 -> shadeColB + 3 -> shadeColA + else -> throw IllegalArgumentException("Invalid channel $channel") + } + var strength: Int = 0 var density: Int = 0 var viscosity: Int = 0 @@ -48,6 +56,16 @@ class BlockProp { inline val luminosity: Cvec get() = BlockPropUtil.getDynamicLumFunc(internalLumCol, dynamicLuminosityFunction) + fun getLum(channel: Int) = BlockPropUtil.getDynamicLumFuncByChan( + when (channel) { + 0 -> lumColR + 1 -> lumColG + 2 -> lumColB + 3 -> lumColA + else -> throw IllegalArgumentException("Invalid channel $channel") + }, dynamicLuminosityFunction, channel + ) + var drop: Int = 0 var maxSupport: Int = -1 // couldn't use NULL at all... diff --git a/src/net/torvald/terrarum/blockproperties/BlockPropUtil.kt b/src/net/torvald/terrarum/blockproperties/BlockPropUtil.kt index 6f0fbaa7e..ea325a84e 100644 --- a/src/net/torvald/terrarum/blockproperties/BlockPropUtil.kt +++ b/src/net/torvald/terrarum/blockproperties/BlockPropUtil.kt @@ -37,22 +37,31 @@ object BlockPropUtil { private fun getTorchFlicker(baseLum: Cvec): Cvec { val funcY = FastMath.interpolateLinear(flickerFuncX / flickerFuncDomain, flickerP0, flickerP1) - return alterBrightnessUniform(baseLum, funcY) } + private fun getTorchFlicker(baseLum: Float): Float { + return baseLum + FastMath.interpolateLinear(flickerFuncX / flickerFuncDomain, flickerP0, flickerP1) + } + private fun getSlowBreath(baseLum: Cvec): Cvec { val funcY = FastMath.sin(FastMath.PI * breathFuncX / breathCycleDuration) * breathRange - return alterBrightnessUniform(baseLum, funcY) } + private fun getSlowBreath(baseLum: Float): Float { + return baseLum + FastMath.sin(FastMath.PI * breathFuncX / breathCycleDuration) * breathRange + } + private fun getPulsate(baseLum: Cvec): Cvec { val funcY = FastMath.sin(FastMath.PI * pulsateFuncX / pulsateCycleDuration) * pulsateRange - return alterBrightnessUniform(baseLum, funcY) } + private fun getPulsate(baseLum: Float): Float { + return baseLum + FastMath.sin(FastMath.PI * pulsateFuncX / pulsateCycleDuration) * pulsateRange + } + /** * Using our own timer so that they flickers for same duration regardless of game's FPS */ @@ -94,6 +103,20 @@ object BlockPropUtil { } } + /** + * @param chan 0 for R, 1 for G, 2 for B, 3 for A + */ + fun getDynamicLumFuncByChan(baseLum: Float, type: Int, chan: Int): Float { + return when (type) { + 1 -> getTorchFlicker(baseLum) + 2 -> (Terrarum.ingame!!.world).globalLight.cpy().mul(LightmapRenderer.DIV_FLOAT).getElem(chan) // current global light + 3 -> WeatherMixer.getGlobalLightOfTime(Terrarum.ingame!!.world, WorldTime.DAY_LENGTH / 2).cpy().mul(LightmapRenderer.DIV_FLOAT).getElem(chan) // daylight at noon + 4 -> getSlowBreath(baseLum) + 5 -> getPulsate(baseLum) + else -> baseLum + } + } + /** * Darken or brighten colour by 'brighten' argument * diff --git a/src/net/torvald/terrarum/worlddrawer/LightmapHDRMap.kt b/src/net/torvald/terrarum/worlddrawer/LightmapHDRMap.kt index 723207663..f193e79c8 100644 --- a/src/net/torvald/terrarum/worlddrawer/LightmapHDRMap.kt +++ b/src/net/torvald/terrarum/worlddrawer/LightmapHDRMap.kt @@ -4,6 +4,9 @@ import com.badlogic.gdx.utils.Disposable import net.torvald.UnsafeHelper /** + * As the fast access to this LUT is critical for the performance because of the way light calculation work, + * even the IO can be a bottleneck so I use UnsafePointer. + * * Created by Torvald on 2019-12-05. */ internal object LightmapHDRMap : Disposable { diff --git a/src/net/torvald/terrarum/worlddrawer/LightmapRendererNew.kt b/src/net/torvald/terrarum/worlddrawer/LightmapRendererNew.kt index 22cf781dc..e631e9d4b 100644 --- a/src/net/torvald/terrarum/worlddrawer/LightmapRendererNew.kt +++ b/src/net/torvald/terrarum/worlddrawer/LightmapRendererNew.kt @@ -20,7 +20,10 @@ import net.torvald.terrarum.gameactors.Luminous import net.torvald.terrarum.gameworld.BlockAddress import net.torvald.terrarum.gameworld.GameWorld import net.torvald.terrarum.modulebasegame.IngameRenderer +import net.torvald.terrarum.modulebasegame.ui.abs import net.torvald.terrarum.realestate.LandUtil +import net.torvald.terrarum.worlddrawer.LightmapRenderer.convX +import net.torvald.terrarum.worlddrawer.LightmapRenderer.convY import kotlin.system.exitProcess /** @@ -226,6 +229,38 @@ object LightmapRenderer { // when disabled, light will "decay out" instead of "instantly out", which can have a cool effect // but the performance boost is measly 0.1 ms on 6700K lightmap.zerofill() + + + // pre-seed the lightmap with known value + for (x in for_x_start - overscan_open + 1..for_x_end + overscan_open - 1) { + for (y in for_y_start - overscan_open + 1..for_y_end + overscan_open - 1) { + val tile = world.getTileFromTerrain(x, y) + val wall = world.getTileFromWall(x, y) + + val lightlevel = if (!BlockCodex[tile].isSolid && !BlockCodex[wall].isSolid) + sunLight.cpy() + else + colourNull.cpy() + // are you a light source? + lightlevel.add(BlockCodex[tile].luminosity) + + // TODO: add lanterns + + // subtract attenuation value + if (!lightlevel.nonZero()) { + lightlevel.sub( + BlockCodex[tile].opacity + ) + } + + val lx = x.convX(); val ly = y.convY() + + lightmap.setR(lx, ly, lightlevel.r) + lightmap.setG(lx, ly, lightlevel.g) + lightmap.setB(lx, ly, lightlevel.b) + lightmap.setA(lx, ly, lightlevel.a) + } + } } // O((5*9)n) == O(n) where n is a size of the map. // Because of inevitable overlaps on the area, it only works with MAX blend @@ -240,7 +275,7 @@ object LightmapRenderer { AppLoader.measureDebugTime("Renderer.LightTotal") { // Round 2 - for (y in for_y_end + overscan_open downTo for_y_start) { + /*for (y in for_y_end + overscan_open downTo for_y_start) { // TODO multithread the following for loop duh for (x in for_x_start - overscan_open..for_x_end) { calculateAndAssign(lightmap, x, y) @@ -266,6 +301,69 @@ object LightmapRenderer { for (x in for_x_start - overscan_open..for_x_end) { calculateAndAssign(lightmap, x, y) } + }*/ + + + // per-channel operation for bit more aggressive optimisation + repeat(4) { rgbaOffset -> + val visitedCells = setOf(-1L) // (x shl 32) or y + + for (x in for_x_start - overscan_open + 1..for_x_end + overscan_open - 1) { + for (y in for_y_start - overscan_open + 1..for_y_end + overscan_open - 1) { + val lx = x.convX() + val ly = y.convY() + + val hash = (x.toLong() shl 32) or y.toLong() + if (visitedCells.contains(hash)) continue + + var thisLightValue = lightmap.channelGet(lx, ly, rgbaOffset) + + + // nearby tiles, namely a b c d e f g h + val tilea = world.getTileFromTerrain(x - 1, y - 1) + val tileb = world.getTileFromTerrain(x, y - 1) + val tilec = world.getTileFromTerrain(x + 1, y - 1) + val tiled = world.getTileFromTerrain(x - 1, y) + val tilee = world.getTileFromTerrain(x + 1, y) + val tilef = world.getTileFromTerrain(x - 1, y + 1) + val tileg = world.getTileFromTerrain(x, y + 1) + val tileh = world.getTileFromTerrain(x + 1, y + 1) + + // max value from plus-shaped neighbouring tiles + thisLightValue = maxOf(thisLightValue, BlockCodex[tileb].getLum(rgbaOffset)) + thisLightValue = maxOf(thisLightValue, BlockCodex[tiled].getLum(rgbaOffset)) + thisLightValue = maxOf(thisLightValue, BlockCodex[tilee].getLum(rgbaOffset)) + thisLightValue = maxOf(thisLightValue, BlockCodex[tileg].getLum(rgbaOffset)) + // max vaule from x-shaped neighbouring tiles + thisLightValue = maxOf(thisLightValue, BlockCodex[tilea].getLum(rgbaOffset) * 0.70710678f) + thisLightValue = maxOf(thisLightValue, BlockCodex[tilec].getLum(rgbaOffset) * 0.70710678f) + thisLightValue = maxOf(thisLightValue, BlockCodex[tilef].getLum(rgbaOffset) * 0.70710678f) + thisLightValue = maxOf(thisLightValue, BlockCodex[tileh].getLum(rgbaOffset) * 0.70710678f) + + lightmap.channelSet(lx, ly, rgbaOffset, thisLightValue) + + // recurse condition + if (lightmap.channelGet(lx - 1, ly - 1, rgbaOffset) < thisLightValue) + spread(x - 1, y - 1, rgbaOffset, visitedCells, 0) + if (lightmap.channelGet(lx, ly - 1, rgbaOffset) < thisLightValue) + spread(x, y - 1, rgbaOffset, visitedCells, 0) + if (lightmap.channelGet(lx + 1, ly - 1, rgbaOffset) < thisLightValue) + spread(x + 1, y - 1, rgbaOffset, visitedCells, 0) + + if (lightmap.channelGet(lx - 1, ly, rgbaOffset) < thisLightValue) + spread(x - 1, y, rgbaOffset, visitedCells, 0) + if (lightmap.channelGet(lx + 1, ly, rgbaOffset) < thisLightValue) + spread(x + 1, y, rgbaOffset, visitedCells, 0) + + if (lightmap.channelGet(lx - 1, ly + 1, rgbaOffset) < thisLightValue) + spread(x - 1, y + 1, rgbaOffset, visitedCells, 0) + if (lightmap.channelGet(lx, ly + 1, rgbaOffset) < thisLightValue) + spread(x, y + 1, rgbaOffset, visitedCells, 0) + if (lightmap.channelGet(lx + 1, ly + 1, rgbaOffset) < thisLightValue) + spread(x + 1, y + 1, rgbaOffset, visitedCells, 0) + + } + } } } } @@ -311,6 +409,69 @@ object LightmapRenderer { // TODO re-init at every resize private lateinit var updateMessages: List> + /** + * @param x x position in the world + * @param y y position in the world + */ + private fun spread(x: Int, y: Int, rgbaOffset: Int, visitedCells: Set, recurseCount: Int) { + val lx = x.convX() + val ly = y.convY() + + val hash = (x.toLong() shl 32) or y.toLong() + + // stop condition + if (visitedCells.contains(hash)) return + if (x !in for_x_start - overscan_open + 1..for_x_end + overscan_open - 1) return + if (y !in for_y_start - overscan_open + 1..for_y_end + overscan_open - 1) return + if (recurseCount > 128) return + + var thisLightValue = lightmap.channelGet(lx, ly, rgbaOffset) + + // nearby tiles, namely a b c d e f g h + val tilea = world.getTileFromTerrain(x - 1, y - 1) + val tileb = world.getTileFromTerrain(x, y - 1) + val tilec = world.getTileFromTerrain(x + 1, y - 1) + val tiled = world.getTileFromTerrain(x - 1, y) + val tilee = world.getTileFromTerrain(x + 1, y) + val tilef = world.getTileFromTerrain(x - 1, y + 1) + val tileg = world.getTileFromTerrain(x, y + 1) + val tileh = world.getTileFromTerrain(x + 1, y + 1) + + // max value from plus-shaped neighbouring tiles + thisLightValue = maxOf(thisLightValue, BlockCodex[tileb].getLum(rgbaOffset)) + thisLightValue = maxOf(thisLightValue, BlockCodex[tiled].getLum(rgbaOffset)) + thisLightValue = maxOf(thisLightValue, BlockCodex[tilee].getLum(rgbaOffset)) + thisLightValue = maxOf(thisLightValue, BlockCodex[tileg].getLum(rgbaOffset)) + // max vaule from x-shaped neighbouring tiles + thisLightValue = maxOf(thisLightValue, BlockCodex[tilea].getLum(rgbaOffset) * 0.70710678f) + thisLightValue = maxOf(thisLightValue, BlockCodex[tilec].getLum(rgbaOffset) * 0.70710678f) + thisLightValue = maxOf(thisLightValue, BlockCodex[tilef].getLum(rgbaOffset) * 0.70710678f) + thisLightValue = maxOf(thisLightValue, BlockCodex[tileh].getLum(rgbaOffset) * 0.70710678f) + + lightmap.channelSet(lx, ly, rgbaOffset, thisLightValue) + + // recurse condition + if (lightmap.channelGet(lx - 1, ly - 1, rgbaOffset) < thisLightValue) + spread(x - 1, y - 1, rgbaOffset, visitedCells, recurseCount + 1) + if (lightmap.channelGet(lx, ly - 1, rgbaOffset) < thisLightValue) + spread(x, y - 1, rgbaOffset, visitedCells, recurseCount + 1) + if (lightmap.channelGet(lx + 1, ly - 1, rgbaOffset) < thisLightValue) + spread(x + 1, y - 1, rgbaOffset, visitedCells, recurseCount + 1) + + if (lightmap.channelGet(lx - 1, ly, rgbaOffset) < thisLightValue) + spread(x - 1, y, rgbaOffset, visitedCells, recurseCount + 1) + if (lightmap.channelGet(lx + 1, ly, rgbaOffset) < thisLightValue) + spread(x + 1, y, rgbaOffset, visitedCells, recurseCount + 1) + + if (lightmap.channelGet(lx - 1, ly + 1, rgbaOffset) < thisLightValue) + spread(x - 1, y + 1, rgbaOffset, visitedCells, recurseCount + 1) + if (lightmap.channelGet(lx, ly + 1, rgbaOffset) < thisLightValue) + spread(x, y + 1, rgbaOffset, visitedCells, recurseCount + 1) + if (lightmap.channelGet(lx + 1, ly + 1, rgbaOffset) < thisLightValue) + spread(x + 1, y + 1, rgbaOffset, visitedCells, recurseCount + 1) + + } + private fun makeUpdateTaskList() { val lightTaskArr = ArrayList() @@ -786,7 +947,10 @@ object LightmapRenderer { hdr(this.a.coerceIn(0f, 1f)) ) - private fun Cvec.nonZero() = this.r + this.g + this.b + this.a > epsilon + private fun Cvec.nonZero() = this.r.abs() > epsilon && + this.g.abs() > epsilon && + this.b.abs() > epsilon && + this.a.abs() > epsilon val histogram: Histogram get() {