mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-07 12:21:52 +09:00
1105 lines
44 KiB
Kotlin
1105 lines
44 KiB
Kotlin
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<Point2i>((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<Cvec>> = 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<Cvec> = 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<BlockAddress, Cvec>((Terrarum.ingame?.ACTORCONTAINER_INITIAL_SIZE ?: 2) * 4)
|
|
|
|
//private val lightsourceMap = ArrayList<Pair<BlockAddress, Cvec>>(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<Long>()
|
|
|
|
internal fun fireRecalculateEvent(vararg actorContainers: List<ActorWithBody>?) {
|
|
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<Array<ThreadedLightmapUpdateMessage>>
|
|
|
|
private fun makeUpdateTaskList() {
|
|
val lightTaskArr = ArrayList<ThreadedLightmapUpdateMessage>()
|
|
|
|
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<out List<ActorWithBody>?>) {
|
|
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<Cvec>(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()
|
|
|