Files
Terrarum/src/net/torvald/terrarum/weather/WeatherMixer.kt
2024-04-14 00:18:30 +09:00

941 lines
35 KiB
Kotlin

package net.torvald.terrarum.weather
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.*
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.graphics.g2d.TextureRegion
import com.badlogic.gdx.graphics.g2d.UnpackedColourSpriteBatch
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.math.Vector3
import com.badlogic.gdx.utils.JsonValue
import com.jme3.math.FastMath
import net.torvald.gdx.graphics.Cvec
import net.torvald.random.HQRNG
import net.torvald.terrarum.*
import net.torvald.terrarum.App.printdbg
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZEF
import net.torvald.terrarum.gameactors.ActorWithBody
import net.torvald.terrarum.gameworld.GameWorld
import net.torvald.terrarum.gameworld.WorldTime
import net.torvald.terrarum.gameworld.WorldTime.Companion.DAY_LENGTH
import net.torvald.terrarum.RNGConsumer
import net.torvald.terrarum.clut.Skybox
import net.torvald.terrarum.clut.Skybox.elevCnt
import net.torvald.terrarum.utils.JsonFetcher
import net.torvald.terrarum.utils.forEachSiblings
import net.torvald.terrarum.weather.WeatherObjectCloud.Companion.ALPHA_ROLLOFF_Z
import net.torvald.terrarum.weather.WeatherObjectCloud.Companion.NEWBORN_GROWTH_TIME
import net.torvald.terrarum.worlddrawer.WorldCamera
import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack
import net.torvald.util.SortedArrayList
import java.io.File
import java.io.FileFilter
import java.lang.Double.doubleToLongBits
import java.lang.Math.toDegrees
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.math.*
/**
* Currently there is a debate whether this module must be part of the engine or the basegame
*/
/**
*
*
* Current, next are there for cross-fading two weathers
*
*
* Building a CLUT:
* Brightest:Darkest must be "around" 10:1
* Is RGBA-formatted (32-bit)
*
* Created by minjaesong on 2016-07-11.
*/
internal object WeatherMixer : RNGConsumer {
val DEFAULT_WEATHER = BaseModularWeather(
"default",
JsonValue(JsonValue.ValueType.`object`),
GdxColorMap(1, 3, Color(0x55aaffff), Color(0xaaffffff.toInt()), Color.WHITE),
GdxColorMap(2, 2, Color.WHITE, Color.WHITE, Color.WHITE, Color.WHITE),
listOf("default"),
0f,
0f,
0f,
0f,
Vector2(1f, 1f),
Vector2(0f, 0f),
listOf(),
floatArrayOf(1f, 1f)
)
override val RNG = HQRNG()
var globalLightOverridden = false
private var forceWindVec: Vector3? = null
val globalLightNow = Cvec(0)
// private val cloudDrawColour = Color()
private val moonlightMax = Cvec(
0.23f,
0.24f,
0.25f,
0.21f
) // actual moonlight is around ~4100K but our mesopic vision makes it appear blueish (wikipedia: Purkinje effect)
// Weather indices
const val WEATHER_GENERIC = "generic"
const val WEATHER_OVERCAST = "overcast"
// TODO add weather classification indices manually
// TODO to save from GL overhead, store lightmap to array; use GdxColorMap
var forceTimeAt: Int? = null
var forceSolarElev: Double? = null
var forceTurbidity: Double? = null
// doesn't work if the png is in greyscale/indexed mode
val starmapTex: TextureRegion = TextureRegion(Texture(Gdx.files.internal("assets/graphics/astrum.png"))).also {
it.texture.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear)
it.texture.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat)
}
private val shaderAstrum =
App.loadShaderFromClasspath("shaders/blendSkyboxStars.vert", "shaders/blendSkyboxStars.frag")
private val shaderClouds = App.loadShaderFromClasspath("shaders/default.vert", "shaders/clouds.frag")
private var astrumOffX = 0f
private var astrumOffY = 0f
// Clouds are merely a response to the current Weatherbox status //
private val clouds = SortedArrayList<WeatherObjectCloud>()
var cloudsSpawned = 0; private set
private var windVector = Vector3(-1f, 0f, 0.1f) // this is a direction vector
val cloudSpawnMax: Int
get() = 256 shl (App.getConfigInt("maxparticles") / 256)
private val skyboxavr = GdxColorMap(Gdx.files.internal("assets/clut/skyboxavr.png"))
override fun loadFromSave(ingame: IngameInstance, s0: Long, s1: Long) {
super.loadFromSave(ingame, s0, s1)
internalReset(s0, s1)
initClouds(ingame.world.weatherbox.currentWeather)
}
fun internalReset(ingame: IngameInstance) {
internalReset(RNG.state0, RNG.state1)
initClouds(ingame.world.weatherbox.currentWeather)
}
fun internalReset(s0: Long, s1: Long) {
globalLightOverridden = false
forceTimeAt = null
forceSolarElev = null
forceTurbidity = null
astrumOffX = s0.and(0xFFFFL).toFloat() / 65535f * starmapTex.regionWidth
astrumOffY = s1.and(0xFFFFL).toFloat() / 65535f * starmapTex.regionHeight
clouds.clear()
cloudsSpawned = 0
forceWindVec = null
windVector = Vector3(-0.98f, 0f, 0.21f)
oldCamPos.set(WorldCamera.camVector)
}
/**
* Part of Ingame update
*/
fun update(delta: Float, player: ActorWithBody?, world: GameWorld) {
if (player == null) return
// currentWeather = weatherList[WEATHER_GENERIC]!![0] // force set weather
world.weatherbox.update(world)
updateWind(world.weatherbox)
updateClouds(delta, world)
if (!globalLightOverridden) {
world.globalLight = WeatherMixer.globalLightNow
}
}
private fun FloatArray.shiftAndPut(f: Float) {
for (k in 1 until this.size) {
this[k - 1] = this[k]
}
this[this.lastIndex] = f
}
private val HALF_PI = 1.5707964f
private val PI = 3.1415927f
private val TWO_PI = 6.2831855f
private val THREE_PI = 9.424778f
// see: https://stackoverflow.com/questions/2708476/rotation-interpolation/14498790#14498790
private fun getShortestAngle(start: Float, end: Float) =
(((((end - if (start < 0f) TWO_PI + start else start) % TWO_PI) + THREE_PI) % TWO_PI) - PI).let {
if (it > PI) it - TWO_PI else it
}
private fun updateWind(weatherbox: Weatherbox) {
val currentWindSpeed = weatherbox.windSpeed.value
val currentWindDir = weatherbox.windDir.value * HALF_PI
// printdbg(this, "Wind speed = $currentWindSpeed")
if (forceWindVec != null) {
windVector.set(forceWindVec)
}
else {
windVector.set(
(cos(currentWindDir) * currentWindSpeed),
0f,
(sin(currentWindDir) * currentWindSpeed)
)
}
}
private val cloudParallaxMultY = -0.035f
private val cloudParallaxMultX = -0.035f
private var cloudUpdateAkku = 0f
private val oldCamPos = Vector2(0f, 0f)
private val camDelta = Vector2(0f, 0f)
val oobMarginR = 1.5f * App.scr.wf
val oobMarginL = -0.5f * App.scr.wf
private val oobMarginY = -0.5f * App.scr.hf
private val DEBUG_CAUSE_OF_DESPAWN = false
private fun updateClouds(delta: Float, world: GameWorld) {
val camvec = WorldCamera.camVector
val camvec2 = camvec.cpy()
val testCamDelta = camvec.cpy().sub(oldCamPos)
val currentWeather = world.weatherbox.currentWeather
// adjust camDelta to accomodate ROUNDWORLD
if (testCamDelta.x.absoluteValue > world.width * TILE_SIZEF / 2f) {
if (testCamDelta.x >= 0)
camvec2.x -= world.width * TILE_SIZEF
else
camvec2.x += world.width * TILE_SIZEF
testCamDelta.set(camvec2.sub(oldCamPos))
}
camDelta.set(testCamDelta)
// try to spawn an cloud
val cloudChanceEveryMin =
60f / (currentWeather.cloudChance * currentWeather.windSpeed) // if chance = 0, the result will be +inf
while (cloudUpdateAkku >= cloudChanceEveryMin) {
cloudUpdateAkku -= cloudChanceEveryMin
val newCloud = tryToSpawnCloud(currentWeather)
// printdbg(this, "New cloud: scrX,Y,Scale=${newCloud?.screenCoord};\tworldXYZ=${newCloud?.pos}")
}
var immDespawnCount = 0
val immDespawnCauses = ArrayList<String>()
// printdbg(this, "Wind vector = $windVector")
// move the clouds
clouds.forEach {
// do parallax scrolling
it.posX += camDelta.x * cloudParallaxMultX
it.posY += camDelta.y * cloudParallaxMultY
it.update(world, windVector)
if (DEBUG_CAUSE_OF_DESPAWN && it.life == 0) {
immDespawnCount += 1
immDespawnCauses.add(it.despawnCode)
}
}
// printdbg(this, "Newborn cloud death rate: $immDespawnCount/$cloudsSpawned")
if (DEBUG_CAUSE_OF_DESPAWN && App.IS_DEVELOPMENT_BUILD && immDespawnCount > cloudsSpawned / 4) {
val despawnCauseStats = HashMap<String, Int>()
immDespawnCauses.forEach {
if (despawnCauseStats[it] != null) {
despawnCauseStats[it] = despawnCauseStats[it]!! + 1
}
else {
despawnCauseStats[it] = 1
}
}
despawnCauseStats.forEach { s, i ->
printdbg(this, "Cause of death -- $s: $i")
}
System.exit(0)
}
// remove clouds that are marked to be despawn
var i = 0
while (true) {
if (i >= clouds.size) break
if (clouds[i].flagToDespawn) {
clouds.removeAt(i)
i -= 1
cloudsSpawned -= 1
}
i += 1
}
cloudUpdateAkku += delta * world.worldTime.timeDelta
oldCamPos.set(camvec)
}
private val scrHscaler = App.scr.height / 720f
private val cloudSizeMult = App.scr.wf / TerrarumScreenSize.defaultW
fun takeUniformRand(range: ClosedFloatingPointRange<Float>) =
FastMath.interpolateLinear(Math.random().toFloat(), range.start, range.endInclusive)
fun takeTriangularRand(range: ClosedFloatingPointRange<Float>) =
FastMath.interpolateLinear((Math.random() + Math.random()).div(2f).toFloat(), range.start, range.endInclusive)
fun takeGaussianRand(range: ClosedFloatingPointRange<Float>) =
FastMath.interpolateLinear(
(Math.random() + Math.random() + Math.random() + Math.random() + Math.random() + Math.random() + Math.random() + Math.random()).div(
8f
).toFloat(), range.start, range.endInclusive
)
/**
* Spawn anywhere on the visible field
*/
private fun getCloudSpawningPosition(cloud: CloudProps, halfCloudSize: Float, windVector: Vector3): Vector3 {
val Z_POW_BASE = ALPHA_ROLLOFF_Z / 4f
val y = takeUniformRand(-cloud.altHigh..-cloud.altLow) * scrHscaler
val z = takeUniformRand(1f..Z_POW_BASE).pow(1.5f) // clouds are more likely to spawn with low Z-value
val xlow = WeatherObjectCloud.screenXtoWorldX(-halfCloudSize, z)
val xhi = WeatherObjectCloud.screenXtoWorldX(App.scr.width + halfCloudSize, z)
val x = takeUniformRand(xlow..xhi)
return Vector3(x, y, z)
}
/**
* Returns random point for clouds to spawn from, in the opposite side of the current wind vector
*/
private fun getCloudSpawningPositionOLD(cloud: CloudProps, halfCloudSize: Float, windVector: Vector3): Vector3 {
val Z_LIM = ALPHA_ROLLOFF_Z
val Z_POW_BASE = ALPHA_ROLLOFF_Z / 4f
val y = takeUniformRand(-cloud.altHigh..-cloud.altLow) * scrHscaler
var windVectorDir = toDegrees(atan2(windVector.z.toDouble(), windVector.x.toDouble())).toFloat() + 180f
if (windVectorDir < 0f) windVectorDir += 360f
windVectorDir /= 90f // full circle: 4
// an "edge" is a line of length 1 drawn into the edge of the square of size 1 (its total edge length will be 4)
// when the windVectorDir is not an integer, the "edge" will take the shape similar to this: ¬
// 'rr' is a point on the "edge", where 0.5 is a middle point in its length
// val rl = (windVectorDir % 1f).let { if (it < 0.5f) -it else it - 1f }.toInt()
// val rh = 1 + (windVectorDir % 1f).let { if (it < 0.5f) it else 1f - it }.toInt()
// choose between rl and rh using (windVectorDir % 1f) as a pivot
// if pivot = 0.3, rL is 70%, and rR is 30% likely
// plug the vote result into the when()
val selectedQuadrant = takeUniformRand(windVectorDir..windVectorDir + 1f)
// printdbg(this, "Dir: $windVectorDir, Rand(${windVectorDir}..${windVectorDir + 1f}) = ${selectedQuadrant.floorToInt()}($selectedQuadrant)")
val rr = takeUniformRand(0f..1f)
return when (selectedQuadrant.floorToInt()) {
-4, 0, 4 -> { // right side of the screen
val z = FastMath.interpolateLinear(rr, 1f, Z_POW_BASE)
.pow(1.5f) // clouds are more likely to spawn with low Z-value
val posXscr = App.scr.width + halfCloudSize
val x = WeatherObjectCloud.screenXtoWorldX(posXscr, z)
Vector3(x, y, z)
}
-3, 1, 5 -> { // z = inf
val z = ALPHA_ROLLOFF_Z
val posXscr = FastMath.interpolateLinear(rr, -halfCloudSize, App.scr.width + halfCloudSize)
val x = WeatherObjectCloud.screenXtoWorldX(posXscr, Z_LIM)
Vector3(x, y, z)
}
-2, 2, 6 -> { // left side of the screen
val z = FastMath.interpolateLinear(rr, Z_POW_BASE, 1f)
.pow(1.5f) // clouds are more likely to spawn with low Z-value
val posXscr = -halfCloudSize
val x = WeatherObjectCloud.screenXtoWorldX(posXscr, z)
Vector3(x, y, z)
}
-1, 3, 7 -> { // z = 0
val posXscr = FastMath.interpolateLinear(rr, -halfCloudSize, App.scr.width + halfCloudSize)
val z = WeatherObjectCloud.worldYtoWorldZforScreenYof0(y)
val x = WeatherObjectCloud.screenXtoWorldX(posXscr, Z_LIM)
Vector3(x, y, z)
}
else -> throw InternalError()
}
}
private fun tryToSpawnCloud(
currentWeather: BaseModularWeather,
precalculatedPos: Vector3? = null,
ageOverride: Int = 0
): WeatherObjectCloud? {
// printdbg(this, "Trying to spawn a cloud... (${cloudsSpawned} / ${cloudSpawnMax})")
return if (cloudsSpawned < cloudSpawnMax) {
val flip = Math.random() < 0.5
val rC = takeUniformRand(0f..1f)
// val rZ = takeUniformRand(1f..ALPHA_ROLLOFF_Z/4f).pow(1.5f) // clouds are more likely to spawn with low Z-value
// val rY = takeUniformRand(-1f..1f)
val rT1 = takeTriangularRand(-1f..1f)
val (rA, rB) = doubleToLongBits(Math.random()).let {
it.ushr(20).and(0xFFFF).toInt() to it.ushr(36).and(0xFFFF).toInt()
}
var cloudsToSpawn: CloudProps? = null
var c = 0
while (c < currentWeather.clouds.size) {
if (rC < currentWeather.clouds[c].probability) {
cloudsToSpawn = currentWeather.clouds[c]
break
}
c += 1
}
cloudsToSpawn?.let { cloud ->
val cloudScale = cloud.getCloudScaleVariance(rT1)
val hCloudSize = (cloud.spriteSheet.tileW * cloudScale) / 2f + 1f
// val posXscr = initX ?: if (cloudDriftVector.x < 0) (App.scr.width + hCloudSize) else -hCloudSize
// val posX = WeatherObjectCloud.screenXtoWorldX(posXscr, rZ)
// val posY = randomPosWithin(-cloud.altHigh..-cloud.altLow, rY) * scrHscaler
val sheetX = rA % cloud.spriteSheet.horizontalCount
val sheetY = rB % cloud.spriteSheet.verticalCount
val cloudGamma = currentWeather.getRandomCloudGamma(takeUniformRand(-1f..1f), takeUniformRand(-1f..1f))
WeatherObjectCloud(
cloud.spriteSheet.get(sheetX, sheetY),
flip,
cloudGamma.x,
cloudGamma.y
).also {
it.scale = cloudScale * cloudSizeMult
it.pos.set(precalculatedPos ?: getCloudSpawningPosition(cloud, hCloudSize, windVector))
// if (precalculatedPos == null) printdbg(this, "Z=${it.posZ}")
// further set the random altitude if required
if (precalculatedPos != null) {
it.pos.y = takeUniformRand(-cloud.altHigh..-cloud.altLow) * scrHscaler
}
it.life = ageOverride
clouds.add(it)
cloudsSpawned += 1
// printdbg(this, "... Spawning ${cloud.category}($sheetX, $sheetY) cloud at pos ${it.pos}, scale ${it.scale}, invGamma ${it.darkness}")
}
}
}
else null
}
private fun initClouds(currentWeather: BaseModularWeather) {
clouds.clear()
cloudsSpawned = 0
// multiplier is an empirical value that depends on the 'rZ'
// it does converge at ~6, but having it as an initial state does not make it stay converged
repeat((currentWeather.cloudChance * 1.333f).ceilToInt()) {
val z =
takeUniformRand(0.1f..ALPHA_ROLLOFF_Z / 4f - 0.1f).pow(1.5f) // clouds are more likely to spawn with low Z-value
val zz =
FastMath.interpolateLinear((z / ALPHA_ROLLOFF_Z) * 0.8f + 0.1f, ALPHA_ROLLOFF_Z / 4f, ALPHA_ROLLOFF_Z)
val x = WeatherObjectCloud.screenXtoWorldX(takeUniformRand(0f..App.scr.wf), zz)
tryToSpawnCloud(currentWeather, Vector3(x, 0f, z), NEWBORN_GROWTH_TIME.toInt())
}
}
internal fun titleScreenInitWeather(weatherbox: Weatherbox) {
weatherbox.initWith(WeatherCodex.getById("titlescreen")!!, Long.MAX_VALUE)
forceWindVec = Vector3(
-0.98f,
0f,
-0.21f
).scl(1f / 30f) // value taken from TitleScreen.kt; search for 'demoWorld.worldTime.timeDelta = '
initClouds(weatherbox.currentWeather)
}
private fun <T> Array<T?>.addAtFreeSpot(obj: T) {
var c = 0
while (true) {
if (this[c] == null) break
c += 1
}
this[c] = obj
}
private var turbidity0 = 1.0
private var turbidity1 = 1.0
private var mornNoonBlend = 0f
/** Interpolated value, controlled by the weatherbox */
var turbidity = 1.0; private set
/** Controlled by todo: something that monitors ground tile compisition */
var albedo = 1.0; private set
private var gH = 1.8f * App.scr.height
// private var gH = 0.8f * App.scr.height
internal var parallaxPos = 0f; private set
private var solarElev = 0.0
private val HALF_DAY = DAY_LENGTH / 2
/**
* Sub-portion of IngameRenderer. You are not supposed to directly deal with this.
*/
internal fun render(frameDelta: Float, camera: OrthographicCamera, batch: FlippingSpriteBatch, world: GameWorld) {
solarElev = if (forceSolarElev != null)
forceSolarElev!!
else if (forceTimeAt != null)
world.worldTime.getSolarElevationAt(world.worldTime.ordinalDay, forceTimeAt!!)
else
world.worldTime.solarElevationDeg
drawSkybox(camera, batch, world)
drawClouds(frameDelta, batch, world)
batch.color = Color.WHITE
}
private val RECIPROCAL_OF_APPARENT_SOLAR_Y_AT_45DEG = 0.000007
private val APPARENT_SOLAR_Y_AT_45DEG = 1.0 / RECIPROCAL_OF_APPARENT_SOLAR_Y_AT_45DEG
/**
* Mathematical model: https://www.desmos.com/calculator/cf6wqwltqq
*/
private fun cloudYtoSolarAlt(cloudY: Double, currentsolarDeg: Double): Double {
fun a(x: Double) = APPARENT_SOLAR_Y_AT_45DEG * tan(Math.toRadians(x))
fun g(x: Double) = Math.toDegrees(atan(RECIPROCAL_OF_APPARENT_SOLAR_Y_AT_45DEG * x))
val phi = currentsolarDeg + CLOUD_SOLARDEG_OFFSET
val x = cloudY
return g(x + a(phi)).bipolarClamp(Skybox.elevMax)
}
/**
* Dependent on the `drawSkybox(camera, batch, world)` for the `cloudDrawColour`
*
*/
private fun drawClouds(frameDelta: Float, batch: SpriteBatch, world: GameWorld) {
val currentWeather = world.weatherbox.currentWeather
val timeNow = (forceTimeAt ?: world.worldTime.TIME_T.toInt()) % WorldTime.DAY_LENGTH
batch.inUse { _ ->
batch.shader = shaderClouds
val shadeLum = (globalLightNow.r * 3f + globalLightNow.g * 4f + globalLightNow.b * 1f) / 8f * 0.5f
batch.shader.setUniformf("shadeCol", shadeLum * 1.05f, shadeLum, shadeLum / 1.05f, 1f)
clouds.forEach {
val altOfSolarRay = cloudYtoSolarAlt(it.posY*-1.0, solarElev)
val cloudCol1 = getGradientCloud(skyboxavr, altOfSolarRay, mornNoonBlend.toDouble(), turbidity, albedo)
val cloudCol2 = getGradientColour2(currentWeather.daylightClut, altOfSolarRay, timeNow, 4)
val cloudDrawColour = lerp(0.7, cloudCol1, cloudCol2) // no srgblerp for performance
val shadiness = (1.0 / cosh(altOfSolarRay * 0.5)).toFloat().coerceAtLeast(if (altOfSolarRay < 0) 0.6666f else 0f)
// printdbg(this, "cloudY=${-it.posY}\tsolarElev=$solarElev\trayAlt=$altOfSolarRay\tshady=$shadiness")
it.render(frameDelta, batch as UnpackedColourSpriteBatch, cloudDrawColour.toGdxColor(), shadiness)
}
}
}
private val parallaxDomainSize = 550f
private val turbidityDomainSize = parallaxDomainSize * 1.3333334f
private val CLOUD_SOLARDEG_OFFSET = 0.3f
private val globalLightBySun = Cvec()
private val globalLightByMoon = Cvec()
private fun drawSkybox(camera: OrthographicCamera, batch: FlippingSpriteBatch, world: GameWorld) {
val weatherbox = world.weatherbox
val currentWeather = world.weatherbox.currentWeather
val parallaxZeroPos = (world.height * 0.4f)
// we will not care for nextSkybox for now
val timeNow = (forceTimeAt ?: world.worldTime.TIME_T.toInt()) % WorldTime.DAY_LENGTH
val daylightClut = currentWeather.daylightClut
// calculate global light
val moonSize = (-(2.0 * world.worldTime.moonPhase - 1.0).abs() + 1.0).toFloat()
globalLightBySun.set(getGradientColour2(daylightClut, solarElev, timeNow))
globalLightByMoon.set(moonlightMax * moonSize)
globalLightNow.set(globalLightBySun max globalLightByMoon)
/* (copied from the shader source)
UV mapping coord.y
-+ <- 1.0 =
D| = // parallax of -1
i| = =
s| = // parallax of 0
p| = =
.| = // parallax of +1
-+ <- 0.0 =
*/
val parallax =
((parallaxZeroPos - WorldCamera.gdxCamY.div(TILE_SIZEF)) / parallaxDomainSize).times(-1f).coerceIn(-1f, 1f)
val turbidityCoeff =
((parallaxZeroPos - WorldCamera.gdxCamY.div(TILE_SIZEF)) / turbidityDomainSize).times(-1f).coerceIn(-1f, 1f)
parallaxPos = parallax
// println(parallax) // parallax value works as intended.
gdxBlendNormalStraightAlpha()
val oldNewBlend = weatherbox.weatherBlend.times(2f).coerceAtMost(1f)
mornNoonBlend =
(1f / 4000f * (timeNow - 43200) + 0.5f).coerceIn(0f, 1f) // 0.0 at T41200; 0.5 at T43200; 1.0 at T45200;
turbidity0 =
(world.weatherbox.oldWeather.json.getDouble("atmoTurbidity") + turbidityCoeff * 2.5).coerceIn(1.0, 10.0)
turbidity1 = (currentWeather.json.getDouble("atmoTurbidity") + turbidityCoeff * 2.5).coerceIn(1.0, 10.0)
turbidity = forceTurbidity ?: FastMath.interpolateLinear(oldNewBlend.toDouble(), turbidity0, turbidity1)
val oldTurbidity = forceTurbidity ?: turbidity0
val thisTurbidity = forceTurbidity ?: turbidity1
albedo = 0.3 // TODO() depends on the ground tile composition
val oldAlbedo = forceTurbidity ?: turbidity0
val thisAlbedo = forceTurbidity ?: turbidity1
/*cloudCol1.set(getGradientCloud(skyboxavr, solarElev, mornNoonBlend.toDouble(), turbidity, albedo))
cloudCol2.set(
getGradientColour2(
daylightClut,
solarElev + CLOUD_SOLARDEG_OFFSET,
timeNow
) max globalLightByMoon
)
cloudDrawColour.set(srgblerp(0.7, cloudCol1, cloudCol2))*/
val gradY = -(gH - App.scr.height) * ((parallax + 1f) / 2f)
val (tex, uvs, turbTihsBlend, albThisBlend, turbOldBlend, albOldBlend) = Skybox.getUV(
solarElev,
oldTurbidity,
oldAlbedo,
thisTurbidity,
thisAlbedo
)
starmapTex.texture.bind(1)
Gdx.gl.glActiveTexture(GL20.GL_TEXTURE0) // so that batch that comes next will bind any tex to it
val astrumX = world.worldTime.axialTiltDeg.toFloat() * starmapTex.regionWidth / 150f
val astrumY = ((world.worldTime.TIME_T / WorldTime.DIURNAL_MOTION_LENGTH) % 1f) * starmapTex.regionHeight
batch.inUse {
batch.shader = shaderAstrum
shaderAstrum.setUniformi("tex1", 1)
shaderAstrum.setUniformf("drawOffsetSize", 0f, gradY, App.scr.wf, gH)
shaderAstrum.setUniform4fv("uvA", uvs, 0, 4)
shaderAstrum.setUniform4fv("uvB", uvs, 4, 4)
shaderAstrum.setUniform4fv("uvC", uvs, 8, 4)
shaderAstrum.setUniform4fv("uvD", uvs, 12, 4)
shaderAstrum.setUniform4fv("uvE", uvs, 16, 4)
shaderAstrum.setUniform4fv("uvF", uvs, 20, 4)
shaderAstrum.setUniform4fv("uvG", uvs, 24, 4)
shaderAstrum.setUniform4fv("uvH", uvs, 28, 4)
shaderAstrum.setUniformf("texBlend1", turbTihsBlend, albThisBlend, turbOldBlend, albOldBlend)
shaderAstrum.setUniformf("texBlend2", oldNewBlend, mornNoonBlend, 0f, 0f)
shaderAstrum.setUniformf("astrumScroll", astrumOffX + astrumX, astrumOffY + astrumY)
shaderAstrum.setUniformf(
"randomNumber",
world.worldTime.TIME_T.div(+14.1f).plus(31L),
world.worldTime.TIME_T.div(-13.8f).plus(37L),
world.worldTime.TIME_T.div(+13.9f).plus(23L),
world.worldTime.TIME_T.div(-14.3f).plus(29L),
)
batch.color = Color.WHITE
batch.draw(tex, 0f, gradY, App.scr.wf, gH, 0f, 0f, 1f, 1f)
batch.color = Color.WHITE
}
}
operator fun Cvec.times(other: Float) = Cvec(this.r * other, this.g * other, this.b * other, this.a * other)
infix fun Cvec.max(other: Cvec) = Cvec(
if (this.r > other.r) this.r else other.r,
if (this.g > other.g) this.g else other.g,
if (this.b > other.b) this.b else other.b,
if (this.a > other.a) this.a else other.a
)
fun colorMix(one: Color, two: Color, scale: Float): Color {
return Color(
FastMath.interpolateLinear(scale, one.r, two.r),
FastMath.interpolateLinear(scale, one.g, two.g),
FastMath.interpolateLinear(scale, one.b, two.b),
FastMath.interpolateLinear(scale, one.a, two.a)
)
}
/**
* Get a GL of specific time
*/
fun getGlobalLightOfTimeOfNoon(currentWeather: BaseModularWeather): Cvec {
currentWeather.daylightClut.let { it.get(it.width - 1, 0) }.let {
return Cvec(it.r, it.g, it.b, it.a)
}
}
fun getGradientColour(world: GameWorld, colorMap: GdxColorMap, row: Int, timeInSec: Int): Cvec {
val dataPointDistance = WorldTime.DAY_LENGTH / colorMap.width
val phaseThis: Int = timeInSec / dataPointDistance // x-coord in gradmap
val phaseNext: Int = (phaseThis + 1) % colorMap.width
val colourThis = colorMap.get(phaseThis % colorMap.width, row)
val colourNext = colorMap.get(phaseNext % colorMap.width, row)
// interpolate R, G, B and A
val scale = (timeInSec % dataPointDistance).toFloat() / dataPointDistance // [0.0, 1.0]
val newCol = colourThis.cpy().lerp(colourNext, scale)//CIELuvUtil.getGradient(scale, colourThis, colourNext)
/* // very nice monitor code
// 65 -> 66 | 300 | 19623 | RGB8(255, 0, 255) -[41%]-> RGB8(193, 97, 23) | * `230`40`160`
// ^ step |width| time | colour from scale colour to | output
if (dataPointDistance == 300)
println("$phaseThis -> $phaseNext | $dataPointDistance | $timeInSec" +
" | ${colourThis.toStringRGB()} -[${scale.times(100).toInt()}%]-> ${colourNext.toStringRGB()}" +
" | * `$r`$g`$b`")*/
return Cvec(newCol)
}
fun getGradientColour2(colorMap: GdxColorMap, solarAngleInDeg: Double, timeOfDay: Int, offY: Int = 0): Cvec {
val pNowRaw = (solarAngleInDeg + 75.0) / 150.0 * colorMap.width
val pStartRaw = pNowRaw.floorToInt()
val pNextRaw = pStartRaw + 1
val pSx: Int;
val pSy: Int;
val pNx: Int;
val pNy: Int
if (timeOfDay < HALF_DAY) {
pSx = pStartRaw.coerceIn(0 until colorMap.width)
pSy = 0
if (pSx == colorMap.width - 1) {
pNx = pSx; pNy = 1
}
else {
pNx = pSx + 1; pNy = 0
}
}
else {
pSx = (pStartRaw + 1).coerceIn(0 until colorMap.width)
pSy = 1
if (pSx == 0) {
pNx = 0; pNy = 0
}
else {
pNx = pSx - 1; pNy = 1
}
}
// interpolate R, G, B and A
var scale = (pNowRaw - pStartRaw).toFloat()
if (timeOfDay >= HALF_DAY) scale = 1f - scale
val colourThisRGB = colorMap.get(pSx, pSy + offY)
val colourNextRGB = colorMap.get(pNx, pNy + offY)
val colourThisUV = colorMap.get(pSx, pSy + 2)
val colourNextUV = colorMap.get(pNx, pNy + 2)
val newColRGB =
colourThisRGB.cpy().lerp(colourNextRGB, scale)//CIELuvUtil.getGradient(scale, colourThis, colourNext)
val newColUV =
colourThisUV.cpy().lerp(colourNextUV, scale)//CIELuvUtil.getGradient(scale, colourThis, colourNext)
return Cvec(newColRGB, newColUV.r)
}
fun getGradientCloud(
colorMap: GdxColorMap,
solarAngleInDeg0: Double,
mornNoonBlend: Double,
turbidity: Double,
albedo: Double
): Cvec {
val solarAngleInDeg = solarAngleInDeg0 + CLOUD_SOLARDEG_OFFSET // add a small offset
val solarAngleInDegInt = solarAngleInDeg.floorToInt()
// fine-grained
val angleX1 = (solarAngleInDegInt + 75).coerceAtMost(150)
val angleX2 = (angleX1 + 1).coerceAtMost(150)
val ax = solarAngleInDeg - solarAngleInDegInt
// fine-grained
val turbY = turbidity.coerceIn(Skybox.turbiditiesD.first(), Skybox.turbiditiesD.last()).minus(1.0)
.times(Skybox.turbDivisor)
val turbY1 = turbY.floorToInt()
val turbY2 = (turbY1 + 1).coerceAtMost(Skybox.turbCnt - 1)
val tx = turbY - turbY1
// coarse-grained
val albX =
albedo.coerceIn(Skybox.albedos.first(), Skybox.albedos.last()).times(5.0) // 0..5
val albX1 = albX.floorToInt() * elevCnt
val albX2 = (albX + 1).floorToInt().coerceAtMost(5) * elevCnt
val bx = ((albX * elevCnt) - albX1) / elevCnt
// println("AngleX: ($angleX1,$angleX2); TurbY: ($turbY1,$turbY2); AlbX: ($albX1,$albX2); MornNoon=(0,${Skybox.albedoCnt * elevCnt}); Albedo: $albedo")
// println("ax=$ax; tx=$tx; bx=$bx")
// println("XY: ${albX1 + angleX1 + Skybox.albedoCnt * elevCnt}, $turbY1")
val a1t1b1A = colorMap.getCvec(albX1 + angleX1, turbY1)
val a2t1b1A = colorMap.getCvec(albX1 + angleX2, turbY1)
val a1t2b1A = colorMap.getCvec(albX1 + angleX1, turbY2)
val a2t2b1A = colorMap.getCvec(albX1 + angleX2, turbY2)
val a1t1b2A = colorMap.getCvec(albX2 + angleX1, turbY1)
val a2t1b2A = colorMap.getCvec(albX2 + angleX2, turbY1)
val a1t2b2A = colorMap.getCvec(albX2 + angleX1, turbY2)
val a2t2b2A = colorMap.getCvec(albX2 + angleX2, turbY2)
val a1t1b1B = colorMap.getCvec(albX1 + angleX1 + Skybox.albedoCnt * elevCnt, turbY1)
val a2t1b1B = colorMap.getCvec(albX1 + angleX2 + Skybox.albedoCnt * elevCnt, turbY1)
val a1t2b1B = colorMap.getCvec(albX1 + angleX1 + Skybox.albedoCnt * elevCnt, turbY2)
val a2t2b1B = colorMap.getCvec(albX1 + angleX2 + Skybox.albedoCnt * elevCnt, turbY2)
val a1t1b2B = colorMap.getCvec(albX2 + angleX1 + Skybox.albedoCnt * elevCnt, turbY1)
val a2t1b2B = colorMap.getCvec(albX2 + angleX2 + Skybox.albedoCnt * elevCnt, turbY1)
val a1t2b2B = colorMap.getCvec(albX2 + angleX1 + Skybox.albedoCnt * elevCnt, turbY2)
val a2t2b2B = colorMap.getCvec(albX2 + angleX2 + Skybox.albedoCnt * elevCnt, turbY2)
// no srgblerp here to match the skybox shader's behaviour
val t1b1A = lerp(ax, a1t1b1A, a2t1b1A)
val t2b1A = lerp(ax, a1t2b1A, a2t2b1A)
val t1b2A = lerp(ax, a1t1b2A, a2t1b2A)
val t2b2A = lerp(ax, a1t2b2A, a2t2b2A)
val t1b1B = lerp(ax, a1t1b1B, a2t1b1B)
val t2b1B = lerp(ax, a1t2b1B, a2t2b1B)
val t1b2B = lerp(ax, a1t1b2B, a2t1b2B)
val t2b2B = lerp(ax, a1t2b2B, a2t2b2B)
val b1A = lerp(tx, t1b1A, t2b1A)
val b2A = lerp(tx, t1b2A, t2b2A)
val b1B = lerp(tx, t1b1B, t2b1B)
val b2B = lerp(tx, t1b2B, t2b2B)
val A = lerp(bx, b1A, b2A)
val B = lerp(bx, b1B, b2B)
return lerp(mornNoonBlend, A, B)
}
private fun lerp(x: Double, c1: Cvec, c2: Cvec): Cvec {
// yes I'm well aware that I must do gamma correction before lerping but it's just tooooo slowwww
val r = (((1.0 - x) * c1.r) + (x * c2.r)).toFloat()
val g = (((1.0 - x) * c1.g) + (x * c2.g)).toFloat()
val b = (((1.0 - x) * c1.b) + (x * c2.b)).toFloat()
val a = (((1.0 - x) * c1.a) + (x * c2.a)).toFloat()
return Cvec(r, g, b, a)
}
private fun srgblerp(x: Double, c1: Cvec, c2: Cvec): Cvec {
return lerp(x, c1.linearise(), c2.linearise()).unlinearise()
}
fun dispose() {
starmapTex.texture.dispose()
shaderAstrum.dispose()
shaderClouds.dispose()
}
private fun Cvec.linearise(): Cvec {
val newR = if (r > 0.04045f)
((r + 0.055f) / 1.055f).pow(2.4f)
else r / 12.92f
val newG = if (g > 0.04045f)
((g + 0.055f) / 1.055f).pow(2.4f)
else g / 12.92f
val newB = if (b > 0.04045f)
((b + 0.055f) / 1.055f).pow(2.4f)
else b / 12.92f
return Cvec(newR, newG, newB, a)
}
private fun Cvec.unlinearise(): Cvec {
val newR = if (r > 0.0031308f)
1.055f * r.pow(1f / 2.4f) - 0.055f
else
r * 12.92f
val newG = if (g > 0.0031308f)
1.055f * g.pow(1f / 2.4f) - 0.055f
else
g * 12.92f
val newB = if (b > 0.0031308f)
1.055f * b.pow(1f / 2.4f) - 0.055f
else
b * 12.92f
return Cvec(newR, newG, newB, a)
}
}
enum class GradientColourMode {
DAYLIGHT, CLOUD_COLOUR
}
private fun Color.set(cvec: Cvec) {
this.r = cvec.r
this.g = cvec.g
this.b = cvec.b
this.a = cvec.a
}