From a3d29b600267b9274a434231a9caf7295a2db0c7 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sat, 23 Sep 2023 01:51:36 +0900 Subject: [PATCH] cloud colour using 'correct' way of lerping for better mix --- .../torvald/terrarum/weather/WeatherMixer.kt | 181 ++++++++++++++---- 1 file changed, 145 insertions(+), 36 deletions(-) diff --git a/src/net/torvald/terrarum/weather/WeatherMixer.kt b/src/net/torvald/terrarum/weather/WeatherMixer.kt index 5e1976c97..3779f4b8f 100644 --- a/src/net/torvald/terrarum/weather/WeatherMixer.kt +++ b/src/net/torvald/terrarum/weather/WeatherMixer.kt @@ -82,7 +82,12 @@ internal object WeatherMixer : RNGConsumer { 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) + 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" @@ -101,7 +106,8 @@ internal object WeatherMixer : RNGConsumer { it.texture.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat) } - private val shaderAstrum = App.loadShaderFromClasspath("shaders/blendSkyboxStars.vert", "shaders/blendSkyboxStars.frag") + 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 @@ -200,7 +206,7 @@ internal object WeatherMixer : RNGConsumer { private fun FloatArray.shiftAndPut(f: Float) { for (k in 1 until this.size) { - this[k-1] = this[k] + this[k - 1] = this[k] } this[this.lastIndex] = f } @@ -241,7 +247,7 @@ internal object WeatherMixer : RNGConsumer { private val oldCamPos = Vector2(0f, 0f) private val camDelta = Vector2(0f, 0f) - val oobMarginR = 1.5f * App.scr.wf + val oobMarginR = 1.5f * App.scr.wf val oobMarginL = -0.5f * App.scr.wf private val oobMarginY = -0.5f * App.scr.hf @@ -266,7 +272,8 @@ internal object WeatherMixer : RNGConsumer { camDelta.set(testCamDelta) // try to spawn an cloud - val cloudChanceEveryMin = 60f / (currentWeather.cloudChance * currentWeather.windSpeed) // if chance = 0, the result will be +inf + val cloudChanceEveryMin = + 60f / (currentWeather.cloudChance * currentWeather.windSpeed) // if chance = 0, the result will be +inf while (cloudUpdateAkku >= cloudChanceEveryMin) { cloudUpdateAkku -= cloudChanceEveryMin @@ -337,10 +344,16 @@ internal object WeatherMixer : RNGConsumer { 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) + 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 + ) /** * Returns random point for clouds to spawn from, in the opposite side of the current wind vector @@ -371,35 +384,45 @@ internal object WeatherMixer : RNGConsumer { 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 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 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) { + private fun tryToSpawnCloud( + currentWeather: BaseModularWeather, + precalculatedPos: Vector3? = null, + ageOverride: Int = 0 + ) { // printdbg(this, "Trying to spawn a cloud... (${cloudsSpawned} / ${cloudSpawnMax})") if (cloudsSpawned < cloudSpawnMax) { @@ -472,9 +495,11 @@ internal object WeatherMixer : RNGConsumer { // 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 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 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) @@ -485,7 +510,11 @@ internal object WeatherMixer : RNGConsumer { internal fun titleScreenInitWeather(weatherbox: Weatherbox) { weatherbox.initWith(weatherDict["titlescreen"]!!, Long.MAX_VALUE) - forceWindVec = Vector3(-0.98f, 0f, -0.21f).scl(1f/30f) // value taken from TitleScreen.kt; search for 'demoWorld.worldTime.timeDelta = ' + forceWindVec = Vector3( + -0.98f, + 0f, + -0.21f + ).scl(1f / 30f) // value taken from TitleScreen.kt; search for 'demoWorld.worldTime.timeDelta = ' initClouds(weatherbox.currentWeather) } @@ -501,6 +530,7 @@ internal object WeatherMixer : RNGConsumer { private var turbidity0 = 1.0 private var turbidity1 = 1.0 + /** Interpolated value, controlled by the weatherbox */ var turbidity = 1.0; private set @@ -513,6 +543,7 @@ internal object WeatherMixer : RNGConsumer { 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. */ @@ -538,7 +569,10 @@ internal object WeatherMixer : RNGConsumer { 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) - batch.shader.setUniformf("shadiness", (1.0 / cosh(solarElev * 0.5)).toFloat().coerceAtLeast(if (solarElev < 0) 0.6666f else 0f)) + batch.shader.setUniformf( + "shadiness", + (1.0 / cosh(solarElev * 0.5)).toFloat().coerceAtLeast(if (solarElev < 0) 0.6666f else 0f) + ) clouds.forEach { it.render(batch as UnpackedColourSpriteBatch, cloudDrawColour) @@ -581,17 +615,21 @@ internal object WeatherMixer : RNGConsumer { .| = // 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) + 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) - val mornNoonBlend = (1f/4000f * (timeNow - 43200) + 0.5f).coerceIn(0f, 1f) // 0.0 at T41200; 0.5 at T43200; 1.0 at T45200; + val 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) + 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 = FastMath.interpolateLinear(oldNewBlend.toDouble(), turbidity0, turbidity1) val oldTurbidity = forceTurbidity ?: turbidity0 @@ -606,14 +644,25 @@ internal object WeatherMixer : RNGConsumer { cloudCol1.set(getGradientCloud(skyboxavr, solarElev, mornNoonBlend.toDouble(), turbidity, albedo)) - cloudCol2.set(getGradientColour2(daylightClut, solarElev + CLOUD_SOLARDEG_OFFSET, timeNow) max globalLightByMoon) - cloudDrawColour.set(lerp(0.5, cloudCol1, cloudCol2)) - + 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) + 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 @@ -636,7 +685,8 @@ internal object WeatherMixer : RNGConsumer { shaderAstrum.setUniformf("texBlend1", turbTihsBlend, albThisBlend, turbOldBlend, albOldBlend) shaderAstrum.setUniformf("texBlend2", oldNewBlend, mornNoonBlend, 0f, 0f) shaderAstrum.setUniformf("astrumScroll", astrumOffX + astrumX, astrumOffY + astrumY) - shaderAstrum.setUniformf("randomNumber", + 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), @@ -661,10 +711,10 @@ internal object WeatherMixer : RNGConsumer { 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) + 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) ) } @@ -708,18 +758,29 @@ internal object WeatherMixer : RNGConsumer { val pStartRaw = pNowRaw.floorToInt() val pNextRaw = pStartRaw + 1 - val pSx: Int; val pSy: Int; val pNx: Int; val pNy: Int + 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 } + 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 } + if (pSx == 0) { + pNx = 0; pNy = 0 + } + else { + pNx = pSx - 1; pNy = 1 + } } // interpolate R, G, B and A var scale = (pNowRaw - pStartRaw).toFloat() @@ -730,13 +791,21 @@ internal object WeatherMixer : RNGConsumer { 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) + 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 { + 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() @@ -745,12 +814,14 @@ internal object WeatherMixer : RNGConsumer { 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 turbY = turbidity.coerceIn(Skybox.turbiditiesD.first(), Skybox.turbiditiesD.last()).minus(1.0) + .times(Skybox.turbDivisor) val turbY1 = turbY.floorToInt() val turbY2 = (turbY1).coerceAtMost(Skybox.turbCnt - 1) val tx = turbY - turbY1 // coarse-grained - val albX = albedo.coerceIn(Skybox.albedos.first(), Skybox.albedos.last()).times(5.0) * Skybox.elevCnt // 0*151..5*151 + val albX = + albedo.coerceIn(Skybox.albedos.first(), Skybox.albedos.last()).times(5.0) * Skybox.elevCnt // 0*151..5*151 val albX1 = albX.floorToInt() val albX2 = (albX1 + 1).coerceAtMost(5 * Skybox.elevCnt) val bx = albX - albX1 @@ -772,6 +843,8 @@ internal object WeatherMixer : RNGConsumer { val a1t2b2B = colorMap.getCvec(albX2 * elevCnt + angleX1 + Skybox.albedoCnt * elevCnt, turbY2) val a2t2b2B = colorMap.getCvec(albX2 * elevCnt + 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) @@ -793,6 +866,7 @@ internal object WeatherMixer : RNGConsumer { } 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() @@ -800,6 +874,10 @@ internal object WeatherMixer : RNGConsumer { 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 getWeatherList(classification: String) = weatherDB[classification]!! fun getRandomWeather(classification: String) = getWeatherList(classification)[RNG.nextInt(getWeatherList(classification).size)] @@ -870,6 +948,37 @@ internal object WeatherMixer : RNGConsumer { 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 {