diff --git a/assets/mods/basegame/weathers/WeatherGeneric.json b/assets/mods/basegame/weathers/WeatherGeneric.json index f31c505f5..014da4d9f 100644 --- a/assets/mods/basegame/weathers/WeatherGeneric.json +++ b/assets/mods/basegame/weathers/WeatherGeneric.json @@ -2,18 +2,20 @@ "skyboxGradColourMap": "generic_skybox.tga", "daylightClut": "clut_daylight.tga", "classification": "generic", - "cloudChance": 20, + "cloudChance": 33, "cloudGamma": [0.48, 1.8], "cloudGammaVariance": [0.1, 0.1], "cloudDriftSpeed": 0.08, "clouds": { - "large": { - "filename": "cloud_large.png", - "tw": 2048, "th": 1024, "probability": 0.2, "baseScale": 0.666, "scaleVariance": 0.7 + "cumulonimbus": { + "filename": "cloud_large.png", "tw": 2048, "th": 1024, "probability": 0.25, + "baseScale": 1.0, "scaleVariance": 0.8, + "altLow": 80, "altHigh": 600 }, - "normal": { - "filename": "cloud_normal.png", - "tw": 1024, "th": 512, "probability": 1.0, "baseScale": 0.75, "scaleVariance": 0.8 + "cumulus": { + "filename": "cloud_normal.png", "tw": 1024, "th": 512, "probability": 1.0, + "baseScale": 1.0, "scaleVariance": 0.6, + "altLow": 80, "altHigh": 800 } } } \ No newline at end of file diff --git a/src/net/torvald/terrarum/ui/BasicDebugInfoWindow.kt b/src/net/torvald/terrarum/ui/BasicDebugInfoWindow.kt index ba8e1a8d3..f01e89709 100644 --- a/src/net/torvald/terrarum/ui/BasicDebugInfoWindow.kt +++ b/src/net/torvald/terrarum/ui/BasicDebugInfoWindow.kt @@ -199,7 +199,7 @@ class BasicDebugInfoWindow : UICanvas() { val soldeg = WeatherMixer.forceSolarElev ?: world?.worldTime?.solarElevationDeg val soldegStr = (soldeg ?: 0.0).toIntAndFrac(3,2) val soldegNeg = ((soldeg ?: 0.0) >= 0.0).toInt() - val turbidity = WeatherMixer.forceTurbidity ?: WeatherMixer.turbidity + val turbidity = (WeatherMixer.forceTurbidity ?: WeatherMixer.turbidity).toIntAndFrac(1, 4) val soldegCol = if (WeatherMixer.forceSolarElev != null) ccO else ccG val turbCol = if (WeatherMixer.forceTurbidity != null) ccO else ccG @@ -300,6 +300,9 @@ class BasicDebugInfoWindow : UICanvas() { if (ingame is TerrarumIngame) { App.fontSmallNumbers.draw(batch, "${ccM}Particles $ccG${(ingame as TerrarumIngame).particlesActive}", (TinyAlphNum.W * 2 + 41 * 8).toFloat(), App.scr.height - TinyAlphNum.H * 2f) + App.fontSmallNumbers.draw(batch, "${ccM}Clouds $ccG${WeatherMixer.cloudsSpawned}", + (TinyAlphNum.W * 2 + 53 * 8).toFloat(), App.scr.height - TinyAlphNum.H * 2f) + } } diff --git a/src/net/torvald/terrarum/weather/BaseModularWeather.kt b/src/net/torvald/terrarum/weather/BaseModularWeather.kt index 226ec3b87..104fd72fb 100644 --- a/src/net/torvald/terrarum/weather/BaseModularWeather.kt +++ b/src/net/torvald/terrarum/weather/BaseModularWeather.kt @@ -32,5 +32,8 @@ data class CloudProps( val spriteSheet: TextureRegionPack, val probability: Float, val baseScale: Float, - val scaleVariance: Float -) \ No newline at end of file + val scaleVariance: Float, + val altLow: Float, + val altHigh: Float, +) { +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/weather/WeatherMixer.kt b/src/net/torvald/terrarum/weather/WeatherMixer.kt index ab3f4d807..7c6313156 100644 --- a/src/net/torvald/terrarum/weather/WeatherMixer.kt +++ b/src/net/torvald/terrarum/weather/WeatherMixer.kt @@ -24,8 +24,10 @@ import net.torvald.terrarum.gameactors.Hitbox import net.torvald.terrarum.spriteassembler.ADPropertyObject 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.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 @@ -89,10 +91,11 @@ internal object WeatherMixer : RNGConsumer { private var astrumOffX = 0f private var astrumOffY = 0f - private val clouds = Array(4096) { null } - private var cloudsSpawned = 0 - private var cloudDriftVector = Vector3(-1f, 0f, 1f) // this is a direction vector - + private val clouds = SortedArrayList() + var cloudsSpawned = 0; private set + private var cloudDriftVector = Vector3(-1f, 0f, 0.1f) // this is a direction vector + private val cloudSpawnMax: Int + get() = App.getConfigInt("maxparticles") * 2 override fun loadFromSave(s0: Long, s1: Long) { super.loadFromSave(s0, s1) @@ -111,9 +114,9 @@ internal object WeatherMixer : RNGConsumer { astrumOffX = s0.and(0xFFFFL).toFloat() / 65535f * starmapTex.regionWidth astrumOffY = s1.and(0xFFFFL).toFloat() / 65535f * starmapTex.regionHeight - Arrays.fill(clouds, null) + clouds.clear() cloudsSpawned = 0 - cloudDriftVector = Vector3(1f, 0f, 0f).rotate(Vector3(0f,1f,0f), 192f) + cloudDriftVector = Vector3(-0.98f, -0.02f, 0.21f) oldCamPos.set(WorldCamera.camVector) } @@ -193,6 +196,10 @@ internal object WeatherMixer : RNGConsumer { 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 fun updateClouds(delta: Float, world: GameWorld) { val camvec = WorldCamera.camVector val camvec2 = camvec.cpy() @@ -218,29 +225,30 @@ internal object WeatherMixer : RNGConsumer { } clouds.forEach { - it?.let { - // do parallax scrolling - it.posX += camDelta.x * cloudParallaxMultX - it.posY += camDelta.y * cloudParallaxMultY + // do parallax scrolling + it.posX += camDelta.x * cloudParallaxMultX + it.posY += camDelta.y * cloudParallaxMultY - it.update(cloudDriftVector, currentWeather.cloudDriftSpeed) - val pjx = it.posX / it.posZ - - if (pjx !in -1500f..App.scr.wf + 1500f || it.scale < 1f / 2048f || it.posZ !in 0.001f..100f) { - it.flagToDespawn = true - } - } + it.update(cloudDriftVector, currentWeather.cloudDriftSpeed) } - clouds.indices.forEach { i -> - if (clouds[i]?.flagToDespawn == true) { - clouds[i]?.dispose() - clouds[i] = null + + // 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 @@ -256,17 +264,25 @@ internal object WeatherMixer : RNGConsumer { private fun randomPosWithin(range: ClosedFloatingPointRange, random: Float) = ((range.start + range.endInclusive) / 2f) + random * (range.endInclusive - range.start) / 2f - private fun tryToSpawnCloud(currentWeather: BaseModularWeather, initX: Float? = null) { - printdbg(this, "Trying to spawn a cloud... (${cloudsSpawned} / ${clouds.size})") + private fun takeUniformRand(range: ClosedFloatingPointRange) = + FastMath.interpolateLinear(Math.random().toFloat(), range.start, range.endInclusive) + private fun takeTriangularRand(range: ClosedFloatingPointRange) = + FastMath.interpolateLinear((Math.random() + Math.random()).div(2f).toFloat(), range.start, range.endInclusive) + private 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) - if (cloudsSpawned < clouds.size) { + + private fun tryToSpawnCloud(currentWeather: BaseModularWeather, initX: Float? = null) { +// printdbg(this, "Trying to spawn a cloud... (${cloudsSpawned} / ${cloudSpawnMax})") + + if (cloudsSpawned < cloudSpawnMax) { val flip = Math.random() < 0.5 - val rC = Math.random().toFloat() - val rZ = (Math.random() * 9 + 1.0).toFloat() // 1..10 uniformly - val rY = ((Math.random() + Math.random()) - 1.0).toFloat() // -1..1 - val r1 = (Math.random() * 2.0 - 1.0).toFloat() // -1..1 - val r2 = (Math.random() * 2.0 - 1.0).toFloat() // -1..1 - val rT1 = ((Math.random() + Math.random()) - 1.0).toFloat() // -1..1 + val rC = takeUniformRand(0f..1f) + val rZ = takeUniformRand(1f..ALPHA_ROLLOFF_Z) + val rY = takeUniformRand(-1f..1f) + val r1 = takeUniformRand(-1f..1f) + val r2 = 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() } @@ -285,11 +301,9 @@ internal object WeatherMixer : RNGConsumer { val scaleVariance = 1f + rT1.absoluteValue * cloud.scaleVariance val cloudScale = cloud.baseScale * (if (rT1 < 0) 1f / scaleVariance else scaleVariance) val hCloudSize = (cloud.spriteSheet.tileW * cloudScale) / 2f + 1f - val posX = if (initX != null) initX * rZ else if (cloudDriftVector.x < 0) (App.scr.width + hCloudSize) * rZ else -hCloudSize * rZ - val posY = when (cloud.category) { - "large" -> randomPosWithin(-120f..120f, rY) * scrHscaler * (rZ * 0.5f) - else -> randomPosWithin(-150f..150f, rY) * scrHscaler * (rZ * 0.5f) - } + 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 WeatherObjectCloud(cloud.spriteSheet.get(sheetX, sheetY), flip).also { @@ -308,11 +322,11 @@ internal object WeatherMixer : RNGConsumer { it.posY = posY it.posZ = rZ - clouds.addAtFreeSpot(it) + clouds.add(it) cloudsSpawned += 1 - printdbg(this, "... Spawning ${cloud.category}($sheetX, $sheetY) cloud at pos ${it.pos}, scale ${it.scale}, invGamma ${it.darkness}") +// printdbg(this, "... Spawning ${cloud.category}($sheetX, $sheetY) cloud at pos ${it.pos}, scale ${it.scale}, invGamma ${it.darkness}") } } @@ -320,8 +334,9 @@ internal object WeatherMixer : RNGConsumer { } private fun initClouds() { - repeat((currentWeather.cloudChance * 6.8f).ceilToInt()) { // multiplier is an empirical value that depends on the 'rZ' - tryToSpawnCloud(currentWeather, ((Math.random() * 2.0 - 1.0) * App.scr.wf).toFloat()) + val hCloudSize = 1024f + repeat((currentWeather.cloudChance * 3.3f).ceilToInt()) { // multiplier is an empirical value that depends on the 'rZ' + tryToSpawnCloud(currentWeather, takeUniformRand(-hCloudSize..App.scr.width + hCloudSize)) } } @@ -353,14 +368,14 @@ internal object WeatherMixer : RNGConsumer { } private fun drawClouds(batch: SpriteBatch) { - batch.color = globalLightNow.toGdxColor().also { - it.a = 1f - } // TODO add cloud-only colour strip on the CLUT batch.shader = shaderClouds - clouds.filterNotNull().sortedByDescending { it.posZ }.forEach { + clouds.forEach { batch.inUse { _ -> + batch.color = globalLightNow.toGdxColor().also { col -> + col.a = it.alpha + } // TODO add cloud-only colour strip on the CLUT batch.shader.setUniformf("gamma", it.darkness) - it.render(batch, 0f, 0f) // TODO parallax + it.render(batch, 0f, 0f) } } } @@ -579,7 +594,9 @@ internal object WeatherMixer : RNGConsumer { TextureRegionPack(ModMgr.getGdxFile(modname, "$pathToImage/${json.getString("filename")}"), json.getInt("tw"), json.getInt("th")), json.getFloat("probability"), json.getFloat("baseScale"), - json.getFloat("scaleVariance") + json.getFloat("scaleVariance"), + json.getFloat("altLow"), + json.getFloat("altHigh"), )) } cloudsMap.sortBy { it.probability } diff --git a/src/net/torvald/terrarum/weather/WeatherObject.kt b/src/net/torvald/terrarum/weather/WeatherObject.kt index 2c00cb4db..27e7a174d 100644 --- a/src/net/torvald/terrarum/weather/WeatherObject.kt +++ b/src/net/torvald/terrarum/weather/WeatherObject.kt @@ -23,6 +23,8 @@ abstract class WeatherObject : Disposable { set(value) { pos.z = value } var scale: Float = 1f + var alpha: Float = 1f + var flagToDespawn = false abstract fun update() diff --git a/src/net/torvald/terrarum/weather/WeatherObjectCloud.kt b/src/net/torvald/terrarum/weather/WeatherObjectCloud.kt index b3e88bb76..2ac103659 100644 --- a/src/net/torvald/terrarum/weather/WeatherObjectCloud.kt +++ b/src/net/torvald/terrarum/weather/WeatherObjectCloud.kt @@ -5,12 +5,13 @@ import com.badlogic.gdx.graphics.g2d.TextureRegion import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.math.Vector3 import net.torvald.terrarum.App +import kotlin.math.pow import kotlin.math.sign /** * Created by minjaesong on 2023-08-21. */ -class WeatherObjectCloud(private val texture: TextureRegion, private val flipW: Boolean) : WeatherObject() { +class WeatherObjectCloud(private val texture: TextureRegion, private val flipW: Boolean) : WeatherObject(), Comparable { /** * To actually utilise this value, your render code must begin the spritebatch per-object, like so: @@ -26,6 +27,7 @@ class WeatherObjectCloud(private val texture: TextureRegion, private val flipW: var darkness: Vector2 = Vector2(0.5f, 2.0f) // the "gamma" value fed into the clouds shader override fun update() { + throw UnsupportedOperationException() } /** @@ -33,30 +35,71 @@ class WeatherObjectCloud(private val texture: TextureRegion, private val flipW: * Resulting vector: (x + dX, y + dY, scale * dScale) */ fun update(flowVector: Vector3, gait: Float) { - val vecMult = Vector3(1f, 1f, 1f / (2f * App.scr.hf * 0.35f)) pos.add(flowVector.cpy().scl(vecMult).scl(gait)) + + alpha = -(posZ / ALPHA_ROLLOFF_Z).pow(1.703f) + 1f + + val lrCoord = screenCoordBottomLR + if (lrCoord.x > WeatherMixer.oobMarginR || lrCoord.z < WeatherMixer.oobMarginL || posZ !in 0.05f..ALPHA_ROLLOFF_Z || alpha < 1f / 255f) { + flagToDespawn = true + } } + private val w = App.scr.halfwf + private val h = App.scr.hf * 0.5f + private val vecMult = Vector3(1f, 1f, 1f / (2f * h)) + /** * X/Y position is a bottom-centre point of the image * Shader must be prepared prior to the render() call */ override fun render(batch: SpriteBatch, offsetX: Float, offsetY: Float) { - val x = posX + offsetX - texture.regionWidth * scale * 0.5f - val y = posY + offsetY - texture.regionHeight * scale - val z = posZ // must be at least 1.0 - val w = App.scr.halfwf - val h = App.scr.hf * 0.35f - - val drawX = (x + w * (z-1)) / z - val drawY = (y + h * (z-1)) / z - val drawScale = scale / z + val sc = screenCoord if (flipW) - batch.draw(texture, drawX + texture.regionWidth / z, drawY, -texture.regionWidth * drawScale, texture.regionHeight * drawScale) + batch.draw(texture, sc.x + texture.regionWidth / posZ, sc.y, -texture.regionWidth * sc.z, texture.regionHeight * sc.z) else - batch.draw(texture, drawX, drawY, texture.regionWidth * drawScale, texture.regionHeight * drawScale) + batch.draw(texture, sc.x, sc.y, texture.regionWidth * sc.z, texture.regionHeight * sc.z) } + /** + * vec3(screen X, screenY, draw scale) + */ + val screenCoord: Vector3 + get() { + val x = posX - texture.regionWidth * scale * 0.5f + val y = posY - texture.regionHeight * scale + val z = posZ // must be at least 1.0 + + val drawX = (x + w * (z-1)) / z + val drawY = (y + h * (z-1)) / z + val drawScale = scale / z + + return Vector3(drawX, drawY, drawScale) + } + + /** + * vec3(screen-X of bottom-left point, screen-Y, screen-X of bottom-right point) + */ + val screenCoordBottomLR: Vector3 + get() { + val xL = posX - texture.regionWidth * scale * 0.5f + val xR = posX + texture.regionWidth * scale * 0.5f + val y = posY - texture.regionHeight * scale + val z = posZ // must be at least 1.0 + + val drawXL = (xL + w * (z-1)) / z + val drawXR = (xR + w * (z-1)) / z + val drawY = (y + h * (z-1)) / z + + return Vector3(drawXL, drawY, drawXR) + } + override fun dispose() { /* cloud texture will be disposed of by the WeatherMixer */ } + override fun compareTo(other: WeatherObjectCloud): Int = (other.posZ - this.posZ).sign.toInt() + + companion object { + fun screenXtoWorldX(screenX: Float, z: Float) = screenX * z - App.scr.halfwf * (z - 1f) + const val ALPHA_ROLLOFF_Z = 16f + } } \ No newline at end of file