diff --git a/assets/mods/basegame/weathers/WeatherGeneric.json b/assets/mods/basegame/weathers/WeatherGeneric.json index 4342afb35..92cb538c4 100644 --- a/assets/mods/basegame/weathers/WeatherGeneric.json +++ b/assets/mods/basegame/weathers/WeatherGeneric.json @@ -1,5 +1,6 @@ { "skyboxGradColourMap": "generic_skybox.tga", + "daylightClut": "clut_daylight.tga", "classification": "generic", "extraImages": [ diff --git a/assets/mods/basegame/weathers/clut_daylight.tga b/assets/mods/basegame/weathers/clut_daylight.tga new file mode 100644 index 000000000..6f595d2de --- /dev/null +++ b/assets/mods/basegame/weathers/clut_daylight.tga @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb1043e011f3683b15aaffee1a31e180f9d8d79a167a96997c8aa1b51348c8e1 +size 1218 diff --git a/src/net/torvald/parametricsky/Application.kt b/src/net/torvald/parametricsky/Application.kt index 6171892e7..30cb43eff 100644 --- a/src/net/torvald/parametricsky/Application.kt +++ b/src/net/torvald/parametricsky/Application.kt @@ -123,12 +123,6 @@ class Application(val WIDTH: Int, val HEIGHT: Int) : Game() { private fun Float.scaleFun() = (1f - 1f / 2f.pow(this/6f)) * 0.97f - private fun Float.negativeElevationScale() = - minOf( - (1f - 1f / 2f.pow(this/6f)) * 0.97f, - 1f - (1f - 1f / 2f.pow(this/6f)) * 0.97f - ) - private fun CIEXYZ.scaleToFit(elevation: Double): CIEXYZ { return if (elevation >= 0) { CIEXYZ( diff --git a/src/net/torvald/terrarum/gameworld/WorldTime.kt b/src/net/torvald/terrarum/gameworld/WorldTime.kt index 539330b5b..16e8c9cb2 100644 --- a/src/net/torvald/terrarum/gameworld/WorldTime.kt +++ b/src/net/torvald/terrarum/gameworld/WorldTime.kt @@ -1,5 +1,9 @@ package net.torvald.terrarum.gameworld +import net.torvald.terrarum.modulebasegame.worldgenerator.TWO_PI +import kotlin.math.cos +import kotlin.math.sin + /** * Please also see: @@ -117,9 +121,22 @@ class WorldTime(initTime: Long = 0L) { inline val moonPhase: Double get() = (TIME_T.plus(1700000L) % LUNAR_CYCLE).toDouble() / LUNAR_CYCLE + val solarElevationDeg: Double + get() { + val x = (TIME_T % YEAR_SECONDS).toDouble() / DAY_LENGTH + 15 // decimal days. One full day = 1.0 + val d = -23.44 * cos(TWO_PI * x / YEAR_DAYS) + + // 51.56 and 23.44 will make yearly min/max elevation to be 75deg + // -0.2504264: a number that makes y=min when x=0 (x=0 is midnight) + return 51.56 * sin(TWO_PI * (x - 0.2504264)) + d + } + val solarElevationRad: Double + get() = Math.toRadians(solarElevationDeg) + @Transient private var realSecAcc: Double = 0.0 @Transient private val REAL_SEC_TO_GAME_SECS = 1.0 / GAME_MIN_TO_REAL_SEC // how slow is real-life clock (second-wise) relative to the ingame one + // NOTE: ingame calendars (the fixture with GUI) should use symbols AND fullnames; the watch already uses shot daynames val DAY_NAMES = arrayOf(//daynames are taken from Nynorsk (å -> o) "Mondag", "Tysdag", "Midtveke" //middle-week , "Torsdag", "Fredag", "Laurdag", "Sundag", "Verddag" //From Norsk word 'verd' @@ -138,19 +155,21 @@ class WorldTime(initTime: Long = 0L) { companion object { /** Each day is displayed as 24 hours, but in real-life clock it's 22 mins long */ - val DAY_LENGTH = 86400 //must be the multiple of 3600 + const val DAY_LENGTH = 86400 //must be the multiple of 3600 - val HOUR_SEC: Int = 3600 - val MINUTE_SEC: Int = 60 - val HOUR_MIN: Int = 60 - val GAME_MIN_TO_REAL_SEC: Double = 720.0 / 11.0 - val HOURS_PER_DAY = DAY_LENGTH / HOUR_SEC + const val HOUR_SEC: Int = 3600 + const val MINUTE_SEC: Int = 60 + const val HOUR_MIN: Int = 60 + const val GAME_MIN_TO_REAL_SEC: Double = 720.0 / 11.0 + const val HOURS_PER_DAY = DAY_LENGTH / HOUR_SEC - val YEAR_DAYS: Int = 120 + const val YEAR_DAYS: Int = 120 - val MONTH_LENGTH = 30 // ingame calendar specific + const val MONTH_LENGTH = 30 // ingame calendar specific - val EPOCH_YEAR = 125 + const val EPOCH_YEAR = 125 + + val YEAR_SECONDS = DAY_LENGTH * YEAR_DAYS /** * Parse a time in the format of "8h30" (hour and minute) or "39882" (second) and return a time of day, in seconds diff --git a/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt b/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt index 865f9f6c0..06472e967 100644 --- a/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt +++ b/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt @@ -28,6 +28,7 @@ import net.torvald.terrarum.gameparticles.ParticleBase import net.torvald.terrarum.gameworld.GameWorld import net.torvald.terrarum.gameworld.WorldSimulator import net.torvald.terrarum.langpack.Lang +import net.torvald.terrarum.modulebasegame.clut.Skybox import net.torvald.terrarum.modulebasegame.gameactors.* import net.torvald.terrarum.modulebasegame.gameactors.physicssolver.CollisionSolver import net.torvald.terrarum.modulebasegame.gameitems.PickaxeCore diff --git a/src/net/torvald/terrarum/modulebasegame/TitleScreen.kt b/src/net/torvald/terrarum/modulebasegame/TitleScreen.kt index dafb27196..6fc96180b 100644 --- a/src/net/torvald/terrarum/modulebasegame/TitleScreen.kt +++ b/src/net/torvald/terrarum/modulebasegame/TitleScreen.kt @@ -27,6 +27,7 @@ import net.torvald.terrarum.gameworld.GameWorld import net.torvald.terrarum.gameworld.WorldTime import net.torvald.terrarum.gameworld.fmod import net.torvald.terrarum.langpack.Lang +import net.torvald.terrarum.modulebasegame.clut.Skybox import net.torvald.terrarum.modulebasegame.ui.UILoadGovernor import net.torvald.terrarum.modulebasegame.ui.UIRemoCon import net.torvald.terrarum.modulebasegame.ui.UITitleRemoConYaml @@ -239,6 +240,7 @@ class TitleScreen(batch: FlippingSpriteBatch) : IngameInstance(batch) { uiContainer.add(uiRemoCon) CommandDict // invoke + Skybox // invoke // TODO add console here diff --git a/src/net/torvald/terrarum/modulebasegame/clut/Skybox.kt b/src/net/torvald/terrarum/modulebasegame/clut/Skybox.kt index 2865e7491..224ae4bba 100644 --- a/src/net/torvald/terrarum/modulebasegame/clut/Skybox.kt +++ b/src/net/torvald/terrarum/modulebasegame/clut/Skybox.kt @@ -1,16 +1,123 @@ package net.torvald.terrarum.modulebasegame.clut +import com.badlogic.gdx.graphics.Pixmap import com.badlogic.gdx.graphics.Texture +import com.badlogic.gdx.utils.Disposable +import net.torvald.colourutil.CIEXYZ +import net.torvald.colourutil.toColor +import net.torvald.colourutil.toRGB +import net.torvald.parametricsky.ArHosekSkyModel +import net.torvald.terrarum.App +import net.torvald.terrarum.App.printdbg +import net.torvald.terrarum.abs +import net.torvald.terrarum.modulebasegame.worldgenerator.HALF_PI +import kotlin.math.* /** * Created by minjaesong on 2023-07-09. */ -object Skybox { +object Skybox : Disposable { - const val gradSize = 64 + const val gradSize = 128 - operator fun get(elevation: Double, turbidity: Double): Array { - TODO() + private val gradTexBin: Array + + operator fun get(elevationDeg: Double, turbidity: Double): Texture { +// if (elevationDeg !in elevationsD) { +// throw IllegalArgumentException("Elevation not in ±75° (got $elevationDeg)") +// } +// if (turbidity !in turbiditiesD) { +// throw IllegalArgumentException("Turbidity not in 1..10 (got $turbidity)") +// } + + val elev = elevationDeg.coerceIn(elevationsD).toInt() - elevations.first + val turb = ((turbidity.coerceIn(turbiditiesD) - turbiditiesD.start) / (turbidities.step / 100.0)).toInt() + +// printdbg(this, "$elevationDeg $turbidity ; $elev $turb") + + return gradTexBin[elev * turbCnt + turb] } + private fun Float.scaleFun() = + (1f - 1f / 2f.pow(this/6f)) * 0.97f + + private fun CIEXYZ.scaleToFit(elevationDeg: Double): CIEXYZ { + return if (elevationDeg >= 0) { + CIEXYZ( + this.X.scaleFun(), + this.Y.scaleFun(), + this.Z.scaleFun(), + this.alpha + ) + } + else { + val elevation1 = -elevationDeg + val elevation2 = -elevationDeg / 28.5 + val scale = (1f - (1f - 1f / 1.8.pow(elevation1)) * 0.97f).toFloat() + val scale2 = (1.0 - (elevation2.pow(E) / E.pow(elevation2))*0.8).toFloat() + CIEXYZ( + this.X.scaleFun() * scale * scale2, + this.Y.scaleFun() * scale * scale2, + this.Z.scaleFun() * scale * scale2, + this.alpha + ) + } + } + + private val elevations = (-75..75) // 151 + private val elevationsD = (elevations.first.toDouble() .. elevations.last.toDouble()) + private val turbidities = (1_00..10_00 step 50) // 19 + private val turbiditiesD = (turbidities.first / 100.0..turbidities.last / 100.0) + private val elevCnt = elevations.count() + private val turbCnt = turbidities.count() + private val albedo = 0.1 + private val gamma = HALF_PI + + private fun Double.mapCircle() = sin(HALF_PI * this) + + init { + printdbg(this, "Initialising skybox model") + + gradTexBin = Array(elevCnt * turbCnt) { + + val elevationDeg = (it / turbCnt).plus(elevations.first).toDouble() + val elevationRad = Math.toRadians(elevationDeg) + val turbidity = 1.0 + (it % turbCnt) / 100.0 + + val state = ArHosekSkyModel.arhosek_xyz_skymodelstate_alloc_init(turbidity, albedo, elevationRad.abs()) + val pixmap = Pixmap(1, gradSize, Pixmap.Format.RGBA8888) + +// printdbg(this, "elev $elevationDeg turb $turbidity") + + for (y in 0 until gradSize) { + val theta = (y.toDouble() / gradSize * 1.0).coerceIn(0.0, 1.0).mapCircle() * HALF_PI + // vertical angle, where 0 is zenith, ±90 is ground (which is odd) + + val xyz = CIEXYZ( + ArHosekSkyModel.arhosek_tristim_skymodel_radiance(state, theta, gamma, 0).toFloat(), + ArHosekSkyModel.arhosek_tristim_skymodel_radiance(state, theta, gamma, 1).toFloat(), + ArHosekSkyModel.arhosek_tristim_skymodel_radiance(state, theta, gamma, 2).toFloat() + ) + val xyz2 = xyz.scaleToFit(elevationDeg) + val rgb = xyz2.toRGB().toColor() + + pixmap.setColor(rgb) + pixmap.drawPixel(0, gradSize - 1 - y) + } + + val texture = Texture(pixmap).also { + it.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear) + } + pixmap.dispose() + texture + } + + App.disposables.add(this) + + printdbg(this, "Skybox model generated!") + } + + override fun dispose() { + gradTexBin.forEach { it.dispose() } + } } \ No newline at end of file diff --git a/src/net/torvald/terrarum/weather/BaseModularWeather.kt b/src/net/torvald/terrarum/weather/BaseModularWeather.kt index eb9ca09e3..94fb88dac 100644 --- a/src/net/torvald/terrarum/weather/BaseModularWeather.kt +++ b/src/net/torvald/terrarum/weather/BaseModularWeather.kt @@ -13,6 +13,7 @@ import java.util.* */ data class BaseModularWeather( var skyboxGradColourMap: GdxColorMap, // row 0: skybox grad top, row 1: skybox grad bottom, row 2: sunlight (RGBA) + val daylightClut: GdxColorMap, val classification: String, var extraImages: ArrayList, val mixFrom: String? = null, diff --git a/src/net/torvald/terrarum/weather/WeatherMixer.kt b/src/net/torvald/terrarum/weather/WeatherMixer.kt index 9562e9115..9d320fc2b 100644 --- a/src/net/torvald/terrarum/weather/WeatherMixer.kt +++ b/src/net/torvald/terrarum/weather/WeatherMixer.kt @@ -1,6 +1,5 @@ package net.torvald.terrarum.weather -import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input import com.badlogic.gdx.graphics.* import com.jme3.math.FastMath @@ -13,8 +12,10 @@ import net.torvald.terrarum.gameactors.ActorWithBody import net.torvald.terrarum.gamecontroller.KeyToggler 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.modulebasegame.RNGConsumer import net.torvald.terrarum.modulebasegame.TerrarumIngame +import net.torvald.terrarum.modulebasegame.clut.Skybox import net.torvald.terrarum.modulebasegame.gameactors.ParticleMegaRain import net.torvald.terrarum.utils.JsonFetcher import net.torvald.terrarum.worlddrawer.WorldCamera @@ -105,9 +106,10 @@ internal object WeatherMixer : RNGConsumer { e.printStackTrace() val defaultWeather = BaseModularWeather( - GdxColorMap(1, 3, Color(0x55aaffff), Color(0xaaffffff.toInt()), Color.WHITE), - "default", - ArrayList() + GdxColorMap(1, 3, Color(0x55aaffff), Color(0xaaffffff.toInt()), Color.WHITE), + GdxColorMap(2, 2, Color.WHITE, Color.WHITE, Color.WHITE, Color.WHITE), + "default", + ArrayList() ) currentWeather = defaultWeather @@ -142,27 +144,28 @@ internal object WeatherMixer : RNGConsumer { if (!globalLightOverridden) { world.globalLight = WeatherMixer.globalLightNow } + } - //private val parallaxZeroPos = WorldGenerator.TERRAIN_AVERAGE_HEIGHT * 0.75f // just an arb multiplier (266.66666 -> 200) - - private val skyboxPixmap = Pixmap(2, 2, Pixmap.Format.RGBA8888) - private var skyboxTexture = Texture(skyboxPixmap) + private var turbidity = 2.0 + private var gH = 2f * App.scr.height + private val HALF_DAY = DAY_LENGTH / 2 /** * Sub-portion of IngameRenderer. You are not supposed to directly deal with this. */ internal fun render(camera: Camera, batch: FlippingSpriteBatch, world: GameWorld) { - val parallaxZeroPos = (world.height / 3f) * 0.8888f - val parallaxDomainSize = world.height / 4f + val parallaxZeroPos = (world.height / 3f) + val parallaxDomainSize = world.height / 6f // we will not care for nextSkybox for now val timeNow = (forceTimeAt ?: world.worldTime.TIME_T.toInt()) % WorldTime.DAY_LENGTH val skyboxColourMap = currentWeather.skyboxGradColourMap + val daylightClut = currentWeather.daylightClut // calculate global light - val globalLight = getGradientColour(world, skyboxColourMap, 2, timeNow) + val globalLight = getGradientColour2(daylightClut, world.worldTime.solarElevationDeg, timeNow) globalLightNow.set(globalLight) @@ -178,56 +181,30 @@ internal object WeatherMixer : RNGConsumer { -+ <- 0.0 = */ val parallax = ((parallaxZeroPos - WorldCamera.gdxCamY.div(TILE_SIZEF)) / parallaxDomainSize).times(-1f).coerceIn(-1f, 1f) - val parallaxSize = 1f / 3f - val parallaxMidpt = (parallax + 1f) / 2f - val parallaxTop = (parallaxMidpt - (parallaxSize / 2f)).coerceIn(0f, 1f) - val parallaxBottom = (parallaxMidpt + (parallaxSize / 2f)).coerceIn(0f, 1f) +// println(parallax) // parallax value works as intended. - //println("$parallaxZeroPos, $parallax | $parallaxTop - $parallaxMidpt - $parallaxBottom | $parallaxDomainSize") - - // draw skybox to provided graphics instance - val colTopmost = getGradientColour(world, skyboxColourMap, 0, timeNow).toGdxColor() - val colBottommost = getGradientColour(world, skyboxColourMap, 1, timeNow).toGdxColor() - val colMid = colorMix(colTopmost, colBottommost, 0.5f) - - //println(parallax) // parallax value works as intended. - - val colTop = colorMix(colTopmost, colBottommost, parallaxTop) - val colBottom = colorMix(colTopmost, colBottommost, parallaxBottom) - - // draw using shaperenderer or whatever - - //Terrarum.textureWhiteSquare.bind(0) gdxBlendNormalStraightAlpha() - // draw to skybox texture - skyboxPixmap.setColor(colBottom) - skyboxPixmap.drawPixel(0, 0); skyboxPixmap.drawPixel(1, 0) - skyboxPixmap.setColor(colTop) - skyboxPixmap.drawPixel(0, 1); skyboxPixmap.drawPixel(1, 1) - skyboxTexture.dispose() - skyboxTexture = Texture(skyboxPixmap); skyboxTexture.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear) + val deg =world.worldTime.solarElevationDeg + val degThis = deg.floor() + val degNext = degThis + if (timeNow < HALF_DAY) 1 else -1 // Skybox.get has internal coerceIn + val texture1 = Skybox.get(degThis, turbidity) + val texture2 = Skybox.get(degNext, turbidity) + val lerpScale = (if (timeNow < HALF_DAY) deg - degThis else 1f - (deg - degThis)).toFloat() + val gradY = -(gH - App.scr.height) * ((parallax + 1f) / 2f) batch.inUse { batch.shader = null - batch.drawFlipped(skyboxTexture, 0f, -App.scr.halfhf, App.scr.wf, App.scr.hf * 2f) // because of how the linear filter works, we extend the image by two + batch.color = Color.WHITE + batch.drawFlipped(texture1, 0f, gradY, App.scr.wf, gH) + + batch.color = Color(1f, 1f, 1f, lerpScale) + batch.drawFlipped(texture2, 0f, gradY, App.scr.wf, gH) + + batch.color = Color.WHITE } - // don't use shader to just fill the whole screen... frag shader will be called a million times and it's best to not burden it - /* - IngameRenderer.shaderSkyboxFill.bind() - IngameRenderer.shaderSkyboxFill.setUniformMatrix("u_projTrans", camera.combined) - IngameRenderer.shaderSkyboxFill.setUniformf("topColor", topCol.r, topCol.g, topCol.b) - IngameRenderer.shaderSkyboxFill.setUniformf("bottomColor", bottomCol.r, bottomCol.g, bottomCol.b) - IngameRenderer.shaderSkyboxFill.setUniformf("parallax", parallax.coerceIn(-1f, 1f)) - IngameRenderer.shaderSkyboxFill.setUniformf("parallax_size", 1f/3f) - IngameRenderer.shaderSkyboxFill.setUniformf("zoomInv", 1f / (Terrarum.ingame?.screenZoom ?: 1f)) - AppLoader.fullscreenQuad.render(IngameRenderer.shaderSkyboxFill, GL20.GL_TRIANGLES) - */ - - - Gdx.gl.glActiveTexture(GL20.GL_TEXTURE0) // so that batch that comes next will bind any tex to it } @@ -275,6 +252,36 @@ internal object WeatherMixer : RNGConsumer { return Cvec(newCol) } + fun getGradientColour2(colorMap: GdxColorMap, solarAngleInDeg: Double, timeOfDay: Int): Cvec { + val pNowRaw = (solarAngleInDeg + 75.0) / 150.0 * colorMap.width + + val pStartRaw = pNowRaw.floorInt() + val pNextRaw = pStartRaw + 1 + + val pSx: Int; val pSy: Int; val pNx: Int; val pNy: Int + if (timeOfDay < HALF_DAY) { + pSx = pStartRaw; pSy = 0 + if (pSx == colorMap.width-1) { pNx = pSx; pNy = 1 } + else { pNx = pNextRaw; pNy = 0 } + } + else { + pSx = pStartRaw; pSy = 1 + if (pSx == 0) { pNx = 0; pNy = 0 } + else { pNx = pSx - 1; pNy = 1 } + } + + val colourThis = colorMap.get(pSx, pSy) + val colourNext = colorMap.get(pNx, pNy) + + // interpolate R, G, B and A + var scale = (pNowRaw - pStartRaw).toFloat() + if (timeOfDay >= HALF_DAY) scale = 1f - scale + + val newCol = colourThis.cpy().lerp(colourNext, scale)//CIELuvUtil.getGradient(scale, colourThis, colourNext) + + return Cvec(newCol) + } + fun getWeatherList(classification: String) = weatherList[classification]!! fun getRandomWeather(classification: String) = getWeatherList(classification)[HQRNG().nextInt(getWeatherList(classification).size)] @@ -299,10 +306,12 @@ internal object WeatherMixer : RNGConsumer { val JSON = JsonFetcher(path) val skyboxInJson = JSON.getString("skyboxGradColourMap") + val lightbox = JSON.getString("daylightClut") val extraImagesPath = JSON.get("extraImages").asStringArray() val skybox = GdxColorMap(ModMgr.getGdxFile("basegame", "$pathToImage/${skyboxInJson}")) + val daylight = GdxColorMap(ModMgr.getGdxFile("basegame", "$pathToImage/${lightbox}")) val extraImages = ArrayList() @@ -327,18 +336,14 @@ internal object WeatherMixer : RNGConsumer { return BaseModularWeather( - skyboxGradColourMap = skybox, - classification = classification, - extraImages = extraImages + skyboxGradColourMap = skybox, + daylightClut = daylight, + classification = classification, + extraImages = extraImages ) } fun dispose() { - try { - skyboxTexture.dispose() - } - catch (e: Throwable) {} - skyboxPixmap.dispose() } }