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() 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() // 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() 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) = FastMath.interpolateLinear(Math.random().toFloat(), range.start, range.endInclusive) fun takeTriangularRand(range: ClosedFloatingPointRange) = FastMath.interpolateLinear((Math.random() + Math.random()).div(2f).toFloat(), range.start, range.endInclusive) fun takeGaussianRand(range: ClosedFloatingPointRange) = 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 Array.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 }