package net.torvald.terrarum.worlddrawer import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Pixmap import com.badlogic.gdx.graphics.Texture import com.badlogic.gdx.graphics.glutils.ShaderProgram import com.jme3.math.FastMath import net.torvald.gdx.graphics.Cvec import net.torvald.gdx.graphics.UnsafeCvecArray import net.torvald.terrarum.* import net.torvald.terrarum.AppLoader.printdbg import net.torvald.terrarum.blockproperties.Block import net.torvald.terrarum.blockproperties.BlockCodex import net.torvald.terrarum.blockproperties.Fluid import net.torvald.terrarum.concurrent.ThreadExecutor import net.torvald.terrarum.concurrent.ThreadParallel import net.torvald.terrarum.concurrent.sliceEvenly import net.torvald.terrarum.gameactors.ActorWBMovable import net.torvald.terrarum.gameactors.ActorWithBody 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 net.torvald.util.SortedArrayList import kotlin.math.sign import kotlin.system.exitProcess /** * Sub-portion of IngameRenderer. You are not supposed to directly deal with this. * * Created by minjaesong on 2016-01-25. */ //typealias RGB10 = Int // NOTE: no Float16 on this thing: 67 kB of memory footage is totally acceptable /** This object should not be called by yourself; must be only being used and manipulated by your * own ingame renderer */ object LightmapRenderer { private const val TILE_SIZE = CreateTileAtlas.TILE_SIZE /** World change is managed by IngameRenderer.setWorld() */ private var world: GameWorld = GameWorld.makeNullWorld() private lateinit var lightCalcShader: ShaderProgram //private val SHADER_LIGHTING = AppLoader.getConfigBoolean("gpulightcalc") /** do not call this yourself! Let your game renderer handle this! */ internal fun internalSetWorld(world: GameWorld) { try { if (this.world != world) { printdbg(this, "World change detected -- old world: ${this.world.hashCode()}, new world: ${world.hashCode()}") /*for (y in 0 until LIGHTMAP_HEIGHT) { for (x in 0 until LIGHTMAP_WIDTH) { lightmap[y][x] = colourNull } }*/ /*for (i in 0 until lightmap.size) { lightmap[i] = colourNull }*/ lightmap.zerofill() makeUpdateTaskList() } } catch (e: UninitializedPropertyAccessException) { // new init, do nothing } finally { this.world = world // fireRecalculateEvent() } } private const val overscan_open: Int = 40 private const val overscan_opaque: Int = 10 // TODO resize(int, int) -aware var LIGHTMAP_WIDTH = (Terrarum.ingame?.ZOOM_MINIMUM ?: 1f).inv().times(AppLoader.screenW).div(TILE_SIZE).ceil() + overscan_open * 2 + 3 var LIGHTMAP_HEIGHT = (Terrarum.ingame?.ZOOM_MINIMUM ?: 1f).inv().times(AppLoader.screenH).div(TILE_SIZE).ceil() + overscan_open * 2 + 3 private val noopMask = HashSet((LIGHTMAP_WIDTH + LIGHTMAP_HEIGHT) * 2) /** * Float value, 1.0 for 1023 */ // it utilises alpha channel to determine brightness of "glow" sprites (so that alpha channel works like UV light) // will use array of array from now on because fuck it; debug-ability > slight framerate drop. 2019-06-01 private var lightmap: UnsafeCvecArray = UnsafeCvecArray(LIGHTMAP_WIDTH, LIGHTMAP_HEIGHT) //private var lightmap: Array> = Array(LIGHTMAP_HEIGHT) { Array(LIGHTMAP_WIDTH) { Cvec(0) } } // Can't use framebuffer/pixmap -- this is a fvec4 array, whereas they are ivec4. //private var lightmap: Array = Array(LIGHTMAP_WIDTH * LIGHTMAP_HEIGHT) { Cvec(0) } // Can't use framebuffer/pixmap -- this is a fvec4 array, whereas they are ivec4. private val lanternMap = HashMap((Terrarum.ingame?.ACTORCONTAINER_INITIAL_SIZE ?: 2) * 4) //private val lightsourceMap = ArrayList>(256) init { LightmapHDRMap.invoke() printdbg(this, "Overscan open: $overscan_open; opaque: $overscan_opaque") } private const val AIR = Block.AIR const val DRAW_TILE_SIZE: Float = CreateTileAtlas.TILE_SIZE / IngameRenderer.lightmapDownsample // color model related constants const val MUL = 1024 // modify this to 1024 to implement 30-bit RGB const val CHANNEL_MAX_DECIMAL = 1f const val MUL_2 = MUL * MUL const val CHANNEL_MAX = MUL - 1 const val CHANNEL_MAX_FLOAT = CHANNEL_MAX.toFloat() const val COLOUR_RANGE_SIZE = MUL * MUL_2 const val MUL_FLOAT = MUL / 256f val MUL_FLOAT_VEC = Cvec(MUL_FLOAT) const val DIV_FLOAT = 256f / MUL val DIV_FLOAT_VEC = Cvec(DIV_FLOAT) internal var for_x_start = 0 internal var for_y_start = 0 internal var for_x_end = 0 internal var for_y_end = 0 internal var for_draw_x_start = 0 internal var for_draw_y_start = 0 internal var for_draw_x_end = 0 internal var for_draw_y_end = 0 /** * @param x world coord * @param y world coord */ private fun inBounds(x: Int, y: Int) = (y - for_y_start + overscan_open in 0 until LIGHTMAP_HEIGHT && x - for_x_start + overscan_open in 0 until LIGHTMAP_WIDTH) /** World coord to array coord */ private inline fun Int.convX() = this - for_x_start + overscan_open /** World coord to array coord */ private inline fun Int.convY() = this - for_y_start + overscan_open /** * Conventional level (multiplied by four) * * @param x world tile coord * @param y world tile coord */ internal fun getLight(x: Int, y: Int): Cvec? { if (!inBounds(x, y)) { return null } else { val x = x.convX() val y = y.convY() return lightmap.getVec(x, y) * MUL_FLOAT_VEC } } private val cellsToUpdate = ArrayList() internal fun fireRecalculateEvent(vararg actorContainers: List?) { try { world.getTileFromTerrain(0, 0) // test inquiry } catch (e: UninitializedPropertyAccessException) { return // quit prematurely } catch (e: NullPointerException) { System.err.println("[LightmapRendererNew.fireRecalculateEvent] Attempted to refer destroyed unsafe array " + "(${world.layerTerrain.ptr})") e.printStackTrace() return // something's wrong but we'll ignore it like a trustful AK } if (world.worldIndex == -1) return for_x_start = WorldCamera.zoomedX / TILE_SIZE // fix for premature lightmap rendering for_y_start = WorldCamera.zoomedY / TILE_SIZE // on topmost/leftmost side for_draw_x_start = WorldCamera.x / TILE_SIZE for_draw_y_start = WorldCamera.y / TILE_SIZE if (WorldCamera.x < 0) for_draw_x_start -= 1 // edge case fix that light shift 1 tile to the left when WorldCamera.x < 0 //if (WorldCamera.x in -(TILE_SIZE - 1)..-1) for_draw_x_start -= 1 // another edge-case fix; we don't need this anymore? for_x_end = for_x_start + WorldCamera.zoomedWidth / TILE_SIZE + 3 for_y_end = for_y_start + WorldCamera.zoomedHeight / TILE_SIZE + 3 // same fix as above for_draw_x_end = for_draw_x_start + WorldCamera.width / TILE_SIZE + 3 for_draw_y_end = for_draw_y_start + WorldCamera.height / TILE_SIZE + 3 //println("$for_x_start..$for_x_end, $for_x\t$for_y_start..$for_y_end, $for_y") AppLoader.measureDebugTime("Renderer.Lanterns") { buildLanternmap(actorContainers) } // usually takes 3000 ns /* * Updating order: * ,--------. ,--+-----. ,-----+--. ,--------. - * |↘ | | | 3| |3 | | | ↙| ↕︎ overscan_open / overscan_opaque * | ,-----+ | | 2 | | 2 | | +-----. | - depending on the noop_mask * | |1 | | |1 | | 1| | | 1| | * | | 2 | | `-----+ +-----' | | 2 | | * | | 3| |↗ | | ↖| |3 | | * `--+-----' `--------' `--------' `-----+--' * round: 1 2 3 4 * for all lightmap[y][x], run in this order: 2-3-4-1 * If you run only 4 sets, orthogonal/diagonal artefacts are bound to occur, */ // set sunlight sunLight = world.globalLight * DIV_FLOAT_VEC // set no-op mask from solidity of the block AppLoader.measureDebugTime("Renderer.LightNoOpMask") { noopMask.clear() buildNoopMask() } // wipe out lightmap AppLoader.measureDebugTime("Renderer.Light0") { //for (k in 0 until lightmap.size) lightmap[k] = colourNull //for (y in 0 until lightmap.size) for (x in 0 until lightmap[0].size) lightmap[y][x] = colourNull // 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() //lightsourceMap.clear() // pre-seed the lightmap with known value /*for (x in for_x_start - overscan_open..for_x_end + overscan_open) { for (y in for_y_start - overscan_open..for_y_end + overscan_open) { 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.maxAndAssign(BlockCodex[tile].lumCol) // there will be a way to slightly optimise this following line but hey, let's make everything working right first... lightlevel.maxAndAssign(lanternMap[LandUtil.getBlockAddr(world, x, y)] ?: colourNull) if (lightlevel.nonZero()) { // mark the tile as a light source lightsourceMap.add(LandUtil.getBlockAddr(world, x, y) to lightlevel) } //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 fun r1() { // Round 1 for (y in for_y_start - overscan_open..for_y_end) { for (x in for_x_start - overscan_open..for_x_end) { calculateAndAssign(lightmap, x, y) } } } fun r2() { // Round 2 for (y in for_y_end + overscan_open downTo for_y_start) { for (x in for_x_start - overscan_open..for_x_end) { calculateAndAssign(lightmap, x, y) } } } fun r3() { // Round 3 for (y in for_y_end + overscan_open downTo for_y_start) { for (x in for_x_end + overscan_open downTo for_x_start) { calculateAndAssign(lightmap, x, y) } } } fun r4() { // Round 4 for (y in for_y_start - overscan_open..for_y_end) { for (x in for_x_end + overscan_open downTo for_x_start) { calculateAndAssign(lightmap, x, y) } } } // each usually takes 8 000 000..12 000 000 miliseconds total when not threaded if (!AppLoader.getConfigBoolean("multithreadedlight")) { // The skipping is dependent on how you get ambient light, // in this case we have 'spillage' due to the fact calculate() samples 3x3 area. AppLoader.measureDebugTime("Renderer.LightTotal") { r3();r4();r1();r2();r3(); // multithread per channel: slower AND that cursed noisy output /*for (channel in 0..3) { ThreadExecutor.submit { // Round 1 for (y in for_y_start - overscan_open..for_y_end) { for (x in for_x_start - overscan_open..for_x_end) { calculateAndAssignCh(lightmap, x, y, channel) } } // Round 2 for (y in for_y_end + overscan_open downTo for_y_start) { for (x in for_x_start - overscan_open..for_x_end) { calculateAndAssignCh(lightmap, x, y, channel) } } // Round 3 for (y in for_y_end + overscan_open downTo for_y_start) { for (x in for_x_end + overscan_open downTo for_x_start) { calculateAndAssignCh(lightmap, x, y, channel) } } // Round 4 for (y in for_y_start - overscan_open..for_y_end) { for (x in for_x_end + overscan_open downTo for_x_start) { calculateAndAssignCh(lightmap, x, y, channel) } } } } ThreadExecutor.join()*/ // ANECDOTES // * Radiate-from-light-source idea is doomed because skippable cells are completely random // * Spread-every-cell idea might work as skippable cells are predictable, and they're related // to the pos of lightsources // * No-op masks cause some ambient ray to disappear when they're on the screen edge // * Naive optimisation (mark-and-iterate) attempt was a disaster //mark cells to update // Round 1 /*cellsToUpdate.clear() lightsourceMap.forEach { (addr, light) -> val (wx, wy) = LandUtil.resolveBlockAddr(world, addr) // mark cells to update for (y in 0 until overscan_open) { for (x in 0 until overscan_open - y) { val lx = (wx + x).convX(); val ly = (wy + y).convY() if (lx in 0 until LIGHTMAP_WIDTH && ly in 0 until LIGHTMAP_HEIGHT) cellsToUpdate.add((ly.toLong() shl 32) or lx.toLong()) } } } cellsToUpdate.forEach { calculateAndAssign(lightmap, it.toInt(), (it shr 32).toInt()) } // Round 2 cellsToUpdate.clear() lightsourceMap.forEach { (addr, light) -> val (wx, wy) = LandUtil.resolveBlockAddr(world, addr) // mark cells to update for (y in 0 downTo -overscan_open + 1) { for (x in 0 until overscan_open + y) { val lx = (wx + x).convX(); val ly = (wy + y).convY() if (lx in 0 until LIGHTMAP_WIDTH && ly in 0 until LIGHTMAP_HEIGHT) cellsToUpdate.add((ly.toLong() shl 32) or lx.toLong()) } } } cellsToUpdate.forEach { calculateAndAssign(lightmap, it.toInt(), (it shr 32).toInt()) } // Round 3 cellsToUpdate.clear() lightsourceMap.forEach { (addr, light) -> val (wx, wy) = LandUtil.resolveBlockAddr(world, addr) // mark cells to update for (y in 0 downTo -overscan_open + 1) { for (x in 0 downTo -overscan_open + 1 - y) { val lx = (wx + x).convX(); val ly = (wy + y).convY() if (lx in 0 until LIGHTMAP_WIDTH && ly in 0 until LIGHTMAP_HEIGHT) cellsToUpdate.add((ly.toLong() shl 32) or lx.toLong()) } } } cellsToUpdate.forEach { calculateAndAssign(lightmap, it.toInt(), (it shr 32).toInt()) } // Round 4 cellsToUpdate.clear() lightsourceMap.forEach { (addr, light) -> val (wx, wy) = LandUtil.resolveBlockAddr(world, addr) // mark cells to update for (y in 0 until overscan_open) { for (x in 0 downTo -overscan_open + 1 + y) { val lx = (wx + x).convX(); val ly = (wy + y).convY() if (lx in 0 until LIGHTMAP_WIDTH && ly in 0 until LIGHTMAP_HEIGHT) cellsToUpdate.add((ly.toLong() shl 32) or lx.toLong()) } } } cellsToUpdate.forEach { calculateAndAssign(lightmap, it.toInt(), (it shr 32).toInt()) }*/ // per-channel operation for bit more aggressive optimisation /*for (lightsource in lightsourceMap) { val (lsx, lsy) = LandUtil.resolveBlockAddr(world, lightsource.first) // lightmap MUST BE PRE-SEEDED from known lightsources! repeat(4) { rgbaOffset -> for (genus in 1..6) { // use of overscan_open for loop limit is completely arbitrary val rimSize = 1 + 2 * genus var skip = true // left side, counterclockwise for (k in 0 until rimSize) { val wx = lsx - genus; val wy = lsy - genus + k skip = skip and radiate(rgbaOffset, wx, wy, lightsource.second,(lsx - wx)*(lsx - wx) + (lsy - wy)*(lsy - wy)) // whenever radiate() returns false (not-skip), skip is permanently fixated as false } // bottom side, counterclockwise for (k in 1 until rimSize) { val wx = lsx - genus + k; val wy = lsy + genus skip = skip and radiate(rgbaOffset, wx, wy, lightsource.second,(lsx - wx)*(lsx - wx) + (lsy - wy)*(lsy - wy)) } // right side, counterclockwise for (k in 1 until rimSize) { val wx = lsx + genus; val wy = lsy + genus - k skip = skip and radiate(rgbaOffset, wx, wy, lightsource.second,(lsx - wx)*(lsx - wx) + (lsy - wy)*(lsy - wy)) } // top side, counterclockwise for (k in 1 until rimSize - 1) { val wx = lsx + genus - k; val wy = lsy - genus skip = skip and radiate(rgbaOffset, wx, wy, lightsource.second,(lsx - wx)*(lsx - wx) + (lsy - wy)*(lsy - wy)) } if (skip) break } } }*/ } } else if (world.worldIndex != -1) { // to avoid updating on the null world val roundsY = arrayOf( (for_y_end + overscan_open downTo for_y_start).sliceEvenly(ThreadParallel.threadCount), (for_y_end + overscan_open downTo for_y_start).sliceEvenly(ThreadParallel.threadCount), (for_y_start - overscan_open..for_y_end).sliceEvenly(ThreadParallel.threadCount), (for_y_start - overscan_open..for_y_end).sliceEvenly(ThreadParallel.threadCount) ) val roundsX = arrayOf( (for_x_start - overscan_open..for_x_end), (for_x_end + overscan_open downTo for_x_start), (for_x_end + overscan_open downTo for_x_start), (for_x_start - overscan_open..for_x_end) ) AppLoader.measureDebugTime("Renderer.LightParallelPre") { for (round in 0..roundsY.lastIndex) { roundsY[round].forEachIndexed { index, yRange -> ThreadParallel.map(index, "lightrender-round${round + 1}") { for (y in yRange) { for (x in roundsX[round]) { calculateAndAssign(lightmap, x, y) } } } } } } AppLoader.measureDebugTime("Renderer.LightParallelRun") { ThreadParallel.startAllWaitForDie() } } } /** * the lightmap is already been seeded with lightsource. * * @return true if skip */ /*private fun radiate(channel: Int, wx: Int, wy: Int, lightsource: Cvec, distSqr: Int): Boolean { val lx = wx.convX(); val ly = wy.convY() if (lx !in 0 until LIGHTMAP_WIDTH || ly !in 0 until LIGHTMAP_HEIGHT) return true val currentLightLevel = lightmap.channelGet(lx, ly, channel) val attenuate = BlockCodex[world.getTileFromTerrain(wx, wy)].getOpacity(channel) var brightestNeighbour = lightmap.channelGet(lx, ly - 1, channel) brightestNeighbour = maxOf(brightestNeighbour, lightmap.channelGet(lx, ly + 1, channel)) brightestNeighbour = maxOf(brightestNeighbour, lightmap.channelGet(lx - 1, ly, channel)) brightestNeighbour = maxOf(brightestNeighbour, lightmap.channelGet(lx + 1, ly, channel)) //brightestNeighbour = maxOf(brightestNeighbour, lightmap.channelGet(lx - 1, ly - 1, channel) * 0.70710678f) //brightestNeighbour = maxOf(brightestNeighbour, lightmap.channelGet(lx - 1, ly + 1, channel) * 0.70710678f) //brightestNeighbour = maxOf(brightestNeighbour, lightmap.channelGet(lx + 1, ly - 1, channel) * 0.70710678f) //brightestNeighbour = maxOf(brightestNeighbour, lightmap.channelGet(lx + 1, ly + 1, channel) * 0.70710678f) val newLight = brightestNeighbour * (1f - attenuate * lightScalingMagic) if (newLight <= currentLightLevel || newLight < 0.125f) return true lightmap.channelSet(lx, ly, channel, newLight) return false } private fun radiate2(lightmap: UnsafeCvecArray, worldX: Int, worldY: Int, lightsource: Cvec): Boolean { if (inNoopMask(worldX, worldY)) return false // just quick snippets to make test work thisTileOpacity = BlockCodex[world.getTileFromTerrain(worldX, worldY)].opacity val x = worldX.convX() val y = worldY.convY() lightLevelThis = lightLevelThis max /* + *///darkenColoured(x - 1, y - 1, thisTileOpacity2) max /* + *///darkenColoured(x + 1, y - 1, thisTileOpacity2) max /* + *///darkenColoured(x - 1, y + 1, thisTileOpacity2) max /* + *///darkenColoured(x + 1, y + 1, thisTileOpacity2) max /* * */darkenColoured(x, y - 1, thisTileOpacity) max /* * */darkenColoured(x, y + 1, thisTileOpacity) max /* * */darkenColoured(x - 1, y, thisTileOpacity) max /* * */darkenColoured(x + 1, y, thisTileOpacity) lightmap.setVec(x, y, lightLevelThis) return false }*/ // TODO re-init at every resize private lateinit var updateMessages: List> private fun makeUpdateTaskList() { val lightTaskArr = ArrayList() val for_x_start = overscan_open val for_y_start = overscan_open val for_x_end = for_x_start + WorldCamera.width / TILE_SIZE + 3 val for_y_end = for_y_start + WorldCamera.height / TILE_SIZE + 3 // same fix as above // Round 2 for (y in for_y_end + overscan_open downTo for_y_start) { for (x in for_x_start - overscan_open..for_x_end) { lightTaskArr.add(ThreadedLightmapUpdateMessage(x, y)) } } // Round 3 for (y in for_y_end + overscan_open downTo for_y_start) { for (x in for_x_end + overscan_open downTo for_x_start) { lightTaskArr.add(ThreadedLightmapUpdateMessage(x, y)) } } // Round 4 for (y in for_y_start - overscan_open..for_y_end) { for (x in for_x_end + overscan_open downTo for_x_start) { lightTaskArr.add(ThreadedLightmapUpdateMessage(x, y)) } } // Round 1 for (y in for_y_start - overscan_open..for_y_end) { for (x in for_x_start - overscan_open..for_x_end) { lightTaskArr.add(ThreadedLightmapUpdateMessage(x, y)) } } updateMessages = lightTaskArr.toTypedArray().sliceEvenly(AppLoader.THREADS) } internal data class ThreadedLightmapUpdateMessage(val x: Int, val y: Int) private fun buildLanternmap(actorContainers: Array?>) { lanternMap.clear() actorContainers.forEach { actorContainer -> actorContainer?.forEach { if (it is Luminous && it is ActorWBMovable) { // put lanterns to the area the luminantBox is occupying for (lightBox in it.lightBoxList) { val lightBoxX = it.hitbox.startX + lightBox.startX val lightBoxY = it.hitbox.startY + lightBox.startY val lightBoxW = lightBox.width val lightBoxH = lightBox.height for (y in lightBoxY.div(TILE_SIZE).floorInt() ..lightBoxY.plus(lightBoxH).div(TILE_SIZE).floorInt()) { for (x in lightBoxX.div(TILE_SIZE).floorInt() ..lightBoxX.plus(lightBoxW).div(TILE_SIZE).floorInt()) { val normalisedCvec = it.color//.cpy().mul(DIV_FLOAT) lanternMap[LandUtil.getBlockAddr(world, x, y)] = normalisedCvec //lanternMap[Point2i(x, y)] = normalisedCvec // Q&D fix for Roundworld anomaly //lanternMap[Point2i(x + world.width, y)] = normalisedCvec //lanternMap[Point2i(x - world.width, y)] = normalisedCvec } } } } } } } private fun buildNoopMask() { fun isShaded(x: Int, y: Int) = try { BlockCodex[world.getTileFromTerrain(x, y)].isSolid } catch (e: NullPointerException) { System.err.println("[LightmapRendererNew.buildNoopMask] Invalid block id ${world.getTileFromTerrain(x, y)} from coord ($x, $y)") e.printStackTrace() false } /* update ordering: clockwise snake for_x_start | 02468>..............|--for_y_start : : : : : : V V 13579>............../--for_y_end | for_x_end */ for (x in for_x_start..for_x_end) { if (isShaded(x, for_y_start)) noopMask.add(Point2i(x, for_y_start)) if (isShaded(x, for_y_end)) noopMask.add(Point2i(x, for_y_end)) } for (y in for_y_start + 1..for_y_end - 1) { if (isShaded(for_x_start, y)) noopMask.add(Point2i(for_x_start, y)) if (isShaded(for_x_end, y)) noopMask.add(Point2i(for_x_end, y)) } } private val ambientAccumulator = Cvec(0f,0f,0f,0f) private var lightLevelThis = Cvec(0f) private var fluidAmountToCol = Cvec(0f) private var thisTileLuminosity = Cvec(0f) private var thisTileOpacity = Cvec(0f) private var thisTileOpacity2 = Cvec(0f) // thisTileOpacity * sqrt(2) private var sunLight = Cvec(0f) private var thisFluid = GameWorld.FluidInfo(Fluid.NULL, 0f) private var thisTerrain = 0 private var thisWall = 0 // per-channel variants private var lightLevelThisCh = 0f private var fluidAmountToColCh = 0f private var thisTileLuminosityCh = 0f private var thisTileOpacityCh = 0f private var thisTileOpacity2Ch = 0f private val SQRT2_VEC = Cvec(1.41421356f) /** * This function will alter following variables: * - lightLevelThis * - thisTerrain * - thisFluid * - thisWall * - thisTileLuminosity * - thisTileOpacity * - thisTileOpacity2 * - sunlight */ private fun getLightsAndShades(x: Int, y: Int) { val (x, y) = world.coerceXY(x, y) lightLevelThis = colourNull thisTerrain = world.getTileFromTerrainRaw(x, y) thisFluid = world.getFluid(x, y) thisWall = world.getTileFromWallRaw(x, y) // regarding the issue #26 try { val fuck = BlockCodex[thisTerrain].getLumCol(x, y) } catch (e: NullPointerException) { System.err.println("## NPE -- x: $x, y: $y, value: $thisTerrain") e.printStackTrace() // create shitty minidump System.err.println("MINIMINIDUMP START") for (xx in x - 16 until x + 16) { val raw = world.getTileFromTerrain(xx, y) val lsb = raw.and(0xff).toString(16).padStart(2, '0') val msb = raw.ushr(8).and(0xff).toString(16).padStart(2, '0') System.err.print(lsb) System.err.print(msb) System.err.print(" ") } System.err.println("\nMINIMINIDUMP END") exitProcess(1) } if (thisFluid.type != Fluid.NULL) { fluidAmountToCol = Cvec(thisFluid.amount) thisTileLuminosity = BlockCodex[thisTerrain].getLumCol(x, y) max (BlockCodex[thisFluid.type].getLumCol(x, y) * fluidAmountToCol) // already been div by four thisTileOpacity = BlockCodex[thisTerrain].opacity max (BlockCodex[thisFluid.type].opacity * fluidAmountToCol) // already been div by four } else { thisTileLuminosity = BlockCodex[thisTerrain].getLumCol(x, y) thisTileOpacity = BlockCodex[thisTerrain].opacity } thisTileOpacity2 = thisTileOpacity * SQRT2_VEC //sunLight.set(world.globalLight); sunLight.mul(DIV_FLOAT) // moved to fireRecalculateEvent() // open air || luminous tile backed by sunlight if ((thisTerrain == AIR && thisWall == AIR) || (thisTileLuminosity.nonZero() && thisWall == AIR)) { lightLevelThis = sunLight } // blend lantern lightLevelThis = lightLevelThis max thisTileLuminosity max (lanternMap[LandUtil.getBlockAddr(world, x, y)] ?: colourNull) } private val inNoopMaskp = Point2i(0,0) private fun inNoopMask(x: Int, y: Int): Boolean { // TODO: digitise your note of the idea of No-op Mask (date unknown, prob before 2017-03-17) if (x in for_x_start..for_x_end) { // if it's in the top flange inNoopMaskp.set(x, for_y_start) if (y < for_y_start - overscan_opaque && noopMask.contains(inNoopMaskp)) return true // if it's in the bottom flange inNoopMaskp.y = for_y_end return (y > for_y_end + overscan_opaque && noopMask.contains(inNoopMaskp)) } else if (y in for_y_start..for_y_end) { // if it's in the left flange inNoopMaskp.set(for_x_start, y) if (x < for_x_start - overscan_opaque && noopMask.contains(inNoopMaskp)) return true // if it's in the right flange inNoopMaskp.set(for_x_end, y) return (x > for_x_end + overscan_opaque && noopMask.contains(inNoopMaskp)) } // top-left corner else if (x < for_x_start && y < for_y_start) { inNoopMaskp.set(for_x_start, for_y_start) return (x < for_x_start - overscan_opaque && y < for_y_start - overscan_opaque && noopMask.contains(inNoopMaskp)) } // top-right corner else if (x > for_x_end && y < for_y_start) { inNoopMaskp.set(for_x_end, for_y_start) return (x > for_x_end + overscan_opaque && y < for_y_start - overscan_opaque && noopMask.contains(inNoopMaskp)) } // bottom-left corner else if (x < for_x_start && y > for_y_end) { inNoopMaskp.set(for_x_start, for_y_end) return (x < for_x_start - overscan_opaque && y > for_y_end + overscan_opaque && noopMask.contains(inNoopMaskp)) } // bottom-right corner else if (x > for_x_end && y > for_y_end) { inNoopMaskp.set(for_x_end, for_y_end) return (x > for_x_end + overscan_opaque && y > for_y_end + overscan_opaque && noopMask.contains(inNoopMaskp)) } else return false // if your IDE error out that you need return statement, AND it's "fixed" by removing 'else' before 'return false', // you're doing it wrong, the IF and return statements must be inclusive. } /** * Calculates the light simulation, using main lightmap as one of the input. */ private fun calculateAndAssign(lightmap: UnsafeCvecArray, worldX: Int, worldY: Int) { //if (inNoopMask(worldX, worldY)) return // O(9n) == O(n) where n is a size of the map getLightsAndShades(worldX, worldY) val x = worldX.convX() val y = worldY.convY() // calculate ambient /* + * + 0 4 1 * * @ * 6 @ 7 * + * + 2 5 3 * sample ambient for eight points and apply attenuation for those * maxblend eight values and use it */ // will "overwrite" what's there in the lightmap if it's the first pass // takes about 2 ms on 6700K lightLevelThis = lightLevelThis max /* + */darkenColoured(x - 1, y - 1, thisTileOpacity2) max /* + */darkenColoured(x + 1, y - 1, thisTileOpacity2) max /* + */darkenColoured(x - 1, y + 1, thisTileOpacity2) max /* + */darkenColoured(x + 1, y + 1, thisTileOpacity2) max /* * */darkenColoured(x, y - 1, thisTileOpacity) max /* * */darkenColoured(x, y + 1, thisTileOpacity) max /* * */darkenColoured(x - 1, y, thisTileOpacity) max /* * */darkenColoured(x + 1, y, thisTileOpacity) //return lightLevelThis.cpy() // it HAS to be a cpy(), otherwise all cells gets the same instance //setLightOf(lightmap, x, y, lightLevelThis.cpy()) lightmap.setVec(x, y, lightLevelThis) } private val SOLIDMULTMAGIC_FALSE = Cvec(1f) private val SOLIDMULTMAGIC_TRUE = Cvec(1.2f) private fun isSolid(x: Int, y: Int): Cvec? { // ...so that they wouldn't appear too dark if (!inBounds(x, y)) return null // brighten if solid return if (BlockCodex[world.getTileFromTerrain(x, y)].isSolid) SOLIDMULTMAGIC_TRUE else SOLIDMULTMAGIC_FALSE } var lightBuffer: Pixmap = Pixmap(1, 1, Pixmap.Format.RGBA8888) private val colourNull = Cvec(0f) private val gdxColorNull = Color(0) private const val epsilon = 1f/1024f private var _lightBufferAsTex: Texture = Texture(1, 1, Pixmap.Format.RGBA8888) internal fun draw(): Texture { // when shader is not used: 0.5 ms on 6700K AppLoader.measureDebugTime("Renderer.LightToScreen") { val this_x_start = for_draw_x_start val this_y_start = for_draw_y_start val this_x_end = for_draw_x_end val this_y_end = for_draw_y_end // wipe out beforehand. You DO need this lightBuffer.blending = Pixmap.Blending.None // gonna overwrite (remove this line causes the world to go bit darker) lightBuffer.setColor(0) lightBuffer.fill() // write to colour buffer for (y in this_y_start..this_y_end) { //println("y: $y, this_y_start: $this_y_start") if (y == this_y_start && this_y_start == 0) { //throw Error("Fuck hits again...") } for (x in this_x_start..this_x_end) { val solidMultMagic = isSolid(x, y) val arrayX = x.convX() val arrayY = y.convY() val color = if (solidMultMagic == null) gdxColorNull else lightmap.getVec(arrayX, arrayY).times(solidMultMagic).toGdxColor().normaliseToHDR() lightBuffer.setColor(color) //lightBuffer.drawPixel(x - this_x_start, y - this_y_start) lightBuffer.drawPixel(x - this_x_start, lightBuffer.height - 1 - y + this_y_start) // flip Y } } // draw to the batch _lightBufferAsTex.dispose() _lightBufferAsTex = Texture(lightBuffer) _lightBufferAsTex.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest) /*Gdx.gl.glActiveTexture(GL20.GL_TEXTURE0) // so that batch that comes next will bind any tex to it // we might not need shader here... //batch.draw(lightBufferAsTex, 0f, 0f, lightBufferAsTex.width.toFloat(), lightBufferAsTex.height.toFloat()) batch.draw(_lightBufferAsTex, 0f, 0f, _lightBufferAsTex.width * DRAW_TILE_SIZE, _lightBufferAsTex.height * DRAW_TILE_SIZE) */ } return _lightBufferAsTex } fun dispose() { LightmapHDRMap.dispose() _lightBufferAsTex.dispose() lightBuffer.dispose() lightmap.destroy() } private val lightScalingMagic = Cvec(8f) private val CVEC_ONE = Cvec(1f) /** * Subtract each channel's RGB value. * * @param x array coord * @param y array coord * @param darken (0-255) per channel * @return darkened data (0-255) per channel */ private fun darkenColoured(x: Int, y: Int, darken: Cvec): Cvec { // use equation with magic number 8.0 // this function, when done recursively (A_x = darken(A_x-1, C)), draws exponential curve. (R^2 = 1) /*return Cvec( data.r * (1f - darken.r * lightScalingMagic),//.clampZero(), data.g * (1f - darken.g * lightScalingMagic),//.clampZero(), data.b * (1f - darken.b * lightScalingMagic),//.clampZero(), data.a * (1f - darken.a * lightScalingMagic))*/ if (x !in 0 until LIGHTMAP_WIDTH || y !in 0 until LIGHTMAP_HEIGHT) return colourNull return lightmap.getVec(x, y) * (CVEC_ONE - darken * lightScalingMagic) } private fun Float.inv() = 1f / this fun Float.floor() = FastMath.floor(this) fun Double.floorInt() = Math.floor(this).toInt() fun Float.round(): Int = Math.round(this) fun Double.round(): Int = Math.round(this).toInt() fun Float.ceil() = FastMath.ceil(this) fun Int.even(): Boolean = this and 1 == 0 fun Int.odd(): Boolean = this and 1 == 1 // TODO: float LUT lookup using linear interpolation // input: 0..1 for int 0..1023 private fun hdr(intensity: Float): Float { val intervalStart = (intensity * CHANNEL_MAX).floorInt() val intervalEnd = (intensity * CHANNEL_MAX).floorInt() + 1 if (intervalStart == intervalEnd) return LightmapHDRMap[intervalStart] val intervalPos = (intensity * CHANNEL_MAX) - (intensity * CHANNEL_MAX).toInt() val ret = interpolateLinear( intervalPos, LightmapHDRMap[intervalStart], LightmapHDRMap[intervalEnd] ) return ret } private var _init = false fun resize(screenW: Int, screenH: Int) { // make sure the BlocksDrawer is resized first! // copied from BlocksDrawer, duh! // FIXME 'lightBuffer' is not zoomable in this way val tilesInHorizontal = (AppLoader.screenWf / TILE_SIZE).ceilInt() + 1 val tilesInVertical = (AppLoader.screenHf / TILE_SIZE).ceilInt() + 1 LIGHTMAP_WIDTH = (Terrarum.ingame?.ZOOM_MINIMUM ?: 1f).inv().times(AppLoader.screenW).div(TILE_SIZE).ceil() + overscan_open * 2 + 3 LIGHTMAP_HEIGHT = (Terrarum.ingame?.ZOOM_MINIMUM ?: 1f).inv().times(AppLoader.screenH).div(TILE_SIZE).ceil() + overscan_open * 2 + 3 if (_init) { lightBuffer.dispose() } else { _init = true } lightBuffer = Pixmap(tilesInHorizontal, tilesInVertical, Pixmap.Format.RGBA8888) lightmap.destroy() lightmap = UnsafeCvecArray(LIGHTMAP_WIDTH, LIGHTMAP_HEIGHT) //lightmap = Array(LIGHTMAP_WIDTH * LIGHTMAP_HEIGHT) { Cvec(0) } printdbg(this, "Resize event") } /** To eliminated visible edge on the gradient when 255/1023 is exceeded */ fun Color.normaliseToHDR() = Color( hdr(this.r.coerceIn(0f, 1f)), hdr(this.g.coerceIn(0f, 1f)), hdr(this.b.coerceIn(0f, 1f)), hdr(this.a.coerceIn(0f, 1f)) ) val histogram: Histogram get() { val reds = IntArray(MUL) // reds[intensity] ← counts val greens = IntArray(MUL) // do. val blues = IntArray(MUL) // do. val uvs = IntArray(MUL) val render_width = for_x_end - for_x_start val render_height = for_y_end - for_y_start // excluiding overscans; only reckon echo lights for (y in overscan_open..render_height + overscan_open + 1) { for (x in overscan_open..render_width + overscan_open + 1) { try { // TODO } catch (e: ArrayIndexOutOfBoundsException) { } } } return Histogram(reds, greens, blues, uvs) } class Histogram(val reds: IntArray, val greens: IntArray, val blues: IntArray, val uvs: IntArray) { val RED = 0 val GREEN = 1 val BLUE = 2 val UV = 3 val screen_tiles: Int = (for_x_end - for_x_start + 2) * (for_y_end - for_y_start + 2) val brightest: Int get() { for (i in CHANNEL_MAX downTo 1) { if (reds[i] > 0 || greens[i] > 0 || blues[i] > 0) return i } return 0 } val brightest8Bit: Int get() { val b = brightest return if (brightest > 255) 255 else b } val dimmest: Int get() { for (i in 0..CHANNEL_MAX) { if (reds[i] > 0 || greens[i] > 0 || blues[i] > 0) return i } return CHANNEL_MAX } val range: Int = CHANNEL_MAX fun get(index: Int): IntArray { return when (index) { RED -> reds GREEN -> greens BLUE -> blues UV -> uvs else -> throw IllegalArgumentException() } } } fun interpolateLinear(scale: Float, startValue: Float, endValue: Float): Float { if (startValue == endValue) { return startValue } if (scale <= 0f) { return startValue } if (scale >= 1f) { return endValue } return (1f - scale) * startValue + scale * endValue } } fun Color.toRGBA() = (255 * r).toInt() shl 24 or ((255 * g).toInt() shl 16) or ((255 * b).toInt() shl 8) or (255 * a).toInt()