diff --git a/assets/mods/basegame/weathers/WeatherGeneric.json b/assets/mods/basegame/weathers/WeatherGeneric.json index 10c13b3ae..82daf78db 100644 --- a/assets/mods/basegame/weathers/WeatherGeneric.json +++ b/assets/mods/basegame/weathers/WeatherGeneric.json @@ -2,15 +2,16 @@ "skyboxGradColourMap": "generic_skybox.tga", "daylightClut": "clut_daylight.tga", "classification": "generic", - "cloudChance": 133, + "cloudChance": 500, "cloudGamma": [0.48, 1.8], "cloudGammaVariance": [0.1, 0.1], "windSpeed": 0.16, + "windSpeedVariance": 1.0, "clouds": { "cumulonimbus": { - "filename": "cloud_large.png", "tw": 2048, "th": 1024, "probability": 0.25, - "baseScale": 1.0, "scaleVariance": 0.8, - "altLow": 80, "altHigh": 600 + "filename": "cloud_large.png", "tw": 2048, "th": 1024, "probability": 0.2, + "baseScale": 2.0, "scaleVariance": 0.3333333, + "altLow": 80, "altHigh": 120 }, "cumulus": { "filename": "cloud_normal.png", "tw": 1024, "th": 512, "probability": 1.0, diff --git a/src/com/jme3/math/FastMath.java b/src/com/jme3/math/FastMath.java index b0c9cf60a..f7688a69c 100644 --- a/src/com/jme3/math/FastMath.java +++ b/src/com/jme3/math/FastMath.java @@ -248,6 +248,10 @@ final public class FastMath { return interpolateCatmullRom(u, 0.5f, p0, p1, p2, p3); } + public static float interpolateCatmullRom(float u, float[] ps) { + return interpolateCatmullRom(u, 0.5f, ps[0], ps[1], ps[2], ps[3]); + } + /**Interpolate a spline between at least 4 control points following the Catmull-Rom equation. * here is the interpolation matrix * m = [ 0.0 1.0 0.0 0.0 ] @@ -331,8 +335,7 @@ final public class FastMath { return l; } - - public static float interpolateHermite(float scale, float p0, float p1, float p2, float p3) { + /*public static float interpolateHermite(float scale, float p0, float p1, float p2, float p3) { // return interpolateHermite(scale, p0, p1, p2, p3, 0f, 0f); float mu2 = scale * scale; float mu3 = mu2 * scale; @@ -350,7 +353,7 @@ final public class FastMath { float a3 = -2*mu3 + 3*mu2 + 0; return a0*p1 + a1*m0 + a2*m1 + a3*p2; - } + }*/ //public static float interpolateHermite(float scale, float p0, float p1, float p2, float p3, float tension, float bias) {} diff --git a/src/net/torvald/terrarum/weather/BaseModularWeather.kt b/src/net/torvald/terrarum/weather/BaseModularWeather.kt index 53357cc55..b8f71a682 100644 --- a/src/net/torvald/terrarum/weather/BaseModularWeather.kt +++ b/src/net/torvald/terrarum/weather/BaseModularWeather.kt @@ -1,9 +1,12 @@ package net.torvald.terrarum.weather 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.terrarum.GdxColorMap import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack +import kotlin.math.absoluteValue /** * Note: Colour maps are likely to have sparse data points @@ -13,27 +16,45 @@ import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack * Created by minjaesong on 2016-07-11. */ data class BaseModularWeather( - val json: JsonValue, - var skyboxGradColourMap: GdxColorMap, // row 0: skybox grad top, row 1: skybox grad bottom, row 2: sunlight (RGBA) - val daylightClut: GdxColorMap, - val classification: String, - val cloudChance: Float, - val windSpeed: Float, - val cloudGamma: Vector2, - val cloudGammaVariance: Vector2, - var clouds: List, // sorted by CloudProps.probability + val json: JsonValue, + var skyboxGradColourMap: GdxColorMap, // row 0: skybox grad top, row 1: skybox grad bottom, row 2: sunlight (RGBA) + val daylightClut: GdxColorMap, + val classification: String, + val cloudChance: Float, + val windSpeed: Float, + val windSpeedVariance: Float, + val cloudGamma: Vector2, + val cloudGammaVariance: Vector2, + var clouds: List, // sorted by CloudProps.probability - val mixFrom: String? = null, - val mixPercentage: Double? = null -) + val mixFrom: String? = null, + val mixPercentage: Double? = null, + + var forceWindVec: Vector3? = null +) { + /** + * @param rnd random number between -1 and +1 + */ + fun getRandomWindSpeed(rnd: Float): Float { + val v = 1f + rnd.absoluteValue * windSpeedVariance + return if (rnd < 0) windSpeed / v else windSpeed * v + } +} data class CloudProps( - val category: String, - val spriteSheet: TextureRegionPack, - val probability: Float, - val baseScale: Float, - val scaleVariance: Float, - val altLow: Float, - val altHigh: Float, + val category: String, + val spriteSheet: TextureRegionPack, + val probability: Float, + val baseScale: Float, + val scaleVariance: Float, + val altLow: Float, + val altHigh: Float, ) { + /** + * @param rnd random number between -1 and +1 + */ + fun getCloudScaleVariance(rnd: Float): Float { + val v = 1f + rnd.absoluteValue * scaleVariance + return if (rnd < 0) baseScale / v else baseScale * v + } } \ 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 f00a9a5f0..f66f90357 100644 --- a/src/net/torvald/terrarum/weather/WeatherMixer.kt +++ b/src/net/torvald/terrarum/weather/WeatherMixer.kt @@ -19,6 +19,7 @@ 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.modulebasegame.worldgenerator.TWO_PI import net.torvald.terrarum.utils.JsonFetcher import net.torvald.terrarum.utils.forEachSiblings import net.torvald.terrarum.weather.WeatherObjectCloud.Companion.ALPHA_ROLLOFF_Z @@ -31,9 +32,7 @@ import java.lang.Double.doubleToLongBits import java.lang.Math.toDegrees import kotlin.collections.ArrayList import kotlin.collections.HashMap -import kotlin.math.absoluteValue -import kotlin.math.atan2 -import kotlin.math.pow +import kotlin.math.* /** * Currently there is a debate whether this module must be part of the engine or the basegame @@ -92,7 +91,7 @@ internal object WeatherMixer : RNGConsumer { private val clouds = SortedArrayList() var cloudsSpawned = 0; private set - private var cloudDriftVector = Vector3(-1f, 0f, 0.1f) // this is a direction vector + private var windVector = Vector3(-1f, 0f, 0.1f) // this is a direction vector val cloudSpawnMax: Int get() = 256 shl (App.getConfigInt("maxparticles") / 256) @@ -116,8 +115,10 @@ internal object WeatherMixer : RNGConsumer { clouds.clear() cloudsSpawned = 0 - cloudDriftVector = Vector3(-0.98f, 0f, 0.21f) -// cloudDriftVector = Vector3(-1f, 0f, -1f) + windVector = Vector3(-0.98f, 0f, 0.21f) + + windDirWindow = null + windSpeedWindow = null oldCamPos.set(WorldCamera.camVector) } @@ -165,6 +166,7 @@ internal object WeatherMixer : RNGConsumer { "default", 0f, 0f, + 0f, Vector2(1f, 1f), Vector2(0f, 0f), listOf() @@ -183,6 +185,7 @@ internal object WeatherMixer : RNGConsumer { // currentWeather = weatherList[WEATHER_GENERIC]!![0] // force set weather + updateWind(delta, world) updateClouds(delta, world) @@ -192,6 +195,81 @@ internal object WeatherMixer : RNGConsumer { } + private fun FloatArray.shiftAndPut(f: Float) { + for (k in 1 until this.size) { + this[k-1] = this[k] + } + this[this.lastIndex] = f + } + + private val PI = 3.1415927f + private val TWO_PI = 6.2831855f + private val THREE_PI = 9.424778f + + private var windDirWindow: FloatArray? = null + private var windSpeedWindow: FloatArray? = null + private val WIND_DIR_TIME_UNIT = 14400 // every 4hr + private val WIND_SPEED_TIME_UNIT = 3600 // every 1hr + private var windDirAkku = 0 // only needed if timeDelta is not divisible by WIND_TIME_UNIT + private var windSpeedAkku = 0 + + // 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(delta: Float, world: GameWorld) { + if (windDirWindow == null) { + windDirWindow = FloatArray(4) { takeUniformRand(-PI..PI) } // completely random regardless of the seed + } + if (windSpeedWindow == null) { + windSpeedWindow = FloatArray(4) { currentWeather.getRandomWindSpeed(takeTriangularRand(-1f..1f)) } // completely random regardless of the seed + } + + val windDirStep = windDirAkku / WIND_DIR_TIME_UNIT.toFloat() + val windSpeedStep = windSpeedAkku / WIND_SPEED_TIME_UNIT.toFloat() + +// val angle0 = windDirWindow[0] +// val angle1 = getShortestAngle(angle0, windDirWindow[1]) +// val angle2 = getShortestAngle(angle1, windDirWindow[2]) +// val angle3 = getShortestAngle(angle2, windDirWindow[3]) +// val fixedAngles = floatArrayOf(angle0, angle1, angle2, angle3) + + val currentWindDir = FastMath.interpolateCatmullRom(windDirStep, windDirWindow) + val currentWindSpeed = FastMath.interpolateCatmullRom(windSpeedStep, windSpeedWindow) + + printdbg(this, + "dir ${Math.toDegrees(currentWindDir.toDouble()).roundToInt()}\t" + + "spd ${currentWindSpeed.times(10f).roundToInt().div(10f)}\t " + + "dirs ${windDirWindow!!.map { Math.toDegrees(it.toDouble()).roundToInt() }} ${windDirStep.times(100).roundToInt()}\t" + + "spds ${windSpeedWindow!!.map { it.times(10f).roundToInt().div(10f) }} ${windSpeedStep.times(100).roundToInt()}" + ) + + if (currentWeather.forceWindVec != null) { + windVector.set(currentWeather.forceWindVec) + } + else { + windVector.set( + cos(currentWindDir) * currentWindSpeed, + 0f, + sin(currentWindDir) * currentWindSpeed + ) + } + + while (windDirAkku >= WIND_DIR_TIME_UNIT) { + windDirAkku -= WIND_DIR_TIME_UNIT + windDirWindow!!.shiftAndPut(takeUniformRand(-PI..PI)) + } + while (windSpeedAkku >= WIND_SPEED_TIME_UNIT) { + windSpeedAkku -= WIND_SPEED_TIME_UNIT + windSpeedWindow!!.shiftAndPut(currentWeather.getRandomWindSpeed(takeTriangularRand(-1f..1f))) + } + + windDirAkku += world.worldTime.timeDelta + windSpeedAkku += world.worldTime.timeDelta + } + private val cloudParallaxMultY = -0.035f private val cloudParallaxMultX = -0.035f private var cloudUpdateAkku = 0f @@ -234,7 +312,7 @@ internal object WeatherMixer : RNGConsumer { it.posY += camDelta.y * cloudParallaxMultY - it.update(cloudDriftVector, currentWeather.windSpeed) + it.update(windVector) if (it.life == 0) immDespawnCount += 1 } @@ -266,13 +344,6 @@ internal object WeatherMixer : RNGConsumer { private val scrHscaler = App.scr.height / 720f private val cloudSizeMult = App.scr.wf / TerrarumScreenSize.defaultW - /** - * @param range: range of the randomised number - * @param random: random number in the range of `[-1, 1]` - */ - private fun randomPosWithin(range: ClosedFloatingPointRange, random: Float) = - ((range.start + range.endInclusive) / 2f) + random * (range.endInclusive - range.start) / 2f - private fun takeUniformRand(range: ClosedFloatingPointRange) = FastMath.interpolateLinear(Math.random().toFloat(), range.start, range.endInclusive) private fun takeTriangularRand(range: ClosedFloatingPointRange) = @@ -284,7 +355,8 @@ internal object WeatherMixer : RNGConsumer { * Returns random point for clouds to spawn from, in the opposite side of the current wind vector */ private fun getCloudSpawningPosition(cloud: CloudProps, halfCloudSize: Float, windVector: Vector3): Vector3 { - val y = randomPosWithin(-cloud.altHigh..-cloud.altLow, takeUniformRand(-1f..1f)) * scrHscaler + val Z_LIM = ALPHA_ROLLOFF_Z/2f + 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 @@ -293,33 +365,40 @@ internal object WeatherMixer : RNGConsumer { // 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 } - val rh = 1f + (windVectorDir % 1f).let { if (it < 0.5f) it else 1f - it } - val rr = windVectorDir + takeUniformRand(rl..rh) -// printdbg(this, "${windVectorDir + rl}..${windVectorDir + rh} / $rr") - val Z_LIM = ALPHA_ROLLOFF_Z/2f - return when (rr.toInt()) { +// 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 % 1f)..(windVectorDir % 1f) + 1f) + +// printdbg(this, "Dir: $windVectorDir, Rand(${windVectorDir % 1f}..${(windVectorDir % 1f) + 1f}) = $selectedQuadrant") + + val rr = takeUniformRand(0f..1f) + + return when (selectedQuadrant.toInt()) { 0, 4 -> { // right side of the screen - val z = FastMath.interpolateLinear(rr % 1f, 1f, ALPHA_ROLLOFF_Z).pow(1.5f) // clouds are more likely to spawn with low Z-value + val z = FastMath.interpolateLinear(rr, 1f, ALPHA_ROLLOFF_Z).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) } 1, 5 -> { // z = inf val z = ALPHA_ROLLOFF_Z - val posXscr = FastMath.interpolateLinear(rr % 1f, App.scr.width + halfCloudSize, -halfCloudSize) + val posXscr = FastMath.interpolateLinear(rr, App.scr.width + halfCloudSize, -halfCloudSize) val x = WeatherObjectCloud.screenXtoWorldX(posXscr, Z_LIM) Vector3(x, y, z) } 2, 6 -> { // left side of the screen - val z = FastMath.interpolateLinear(rr % 1f, ALPHA_ROLLOFF_Z, 1f).pow(1.5f) // clouds are more likely to spawn with low Z-value + val z = FastMath.interpolateLinear(rr, ALPHA_ROLLOFF_Z, 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) } 3, 7 -> { // z = 0 val z = 0.1f - val posXscr = FastMath.interpolateLinear(rr % 1f, -halfCloudSize, App.scr.width + halfCloudSize) + val posXscr = FastMath.interpolateLinear(rr, -halfCloudSize, App.scr.width + halfCloudSize) val x = WeatherObjectCloud.screenXtoWorldX(posXscr, Z_LIM) Vector3(x, y, z) } @@ -352,8 +431,7 @@ internal object WeatherMixer : RNGConsumer { } cloudsToSpawn?.let { cloud -> - val scaleVariance = 1f + rT1.absoluteValue * cloud.scaleVariance - val cloudScale = cloud.baseScale * (if (rT1 < 0) 1f / scaleVariance else scaleVariance) + 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 @@ -365,11 +443,11 @@ internal object WeatherMixer : RNGConsumer { WeatherObjectCloud(cloud.spriteSheet.get(sheetX, sheetY), flip).also { it.scale = cloudScale * cloudSizeMult - it.pos.set(precalculatedPos ?: getCloudSpawningPosition(cloud, hCloudSize, cloudDriftVector)) + it.pos.set(precalculatedPos ?: getCloudSpawningPosition(cloud, hCloudSize, windVector)) // further set the random altitude if required if (precalculatedPos != null) { - it.pos.y = randomPosWithin(-cloud.altHigh..-cloud.altLow, takeUniformRand(-1f..1f)) * scrHscaler + it.pos.y = takeUniformRand(-cloud.altHigh..-cloud.altLow) * scrHscaler } clouds.add(it) @@ -385,10 +463,12 @@ internal object WeatherMixer : RNGConsumer { private fun initClouds() { val hCloudSize = 1024f - repeat((currentWeather.cloudChance * 3.3f).ceilToInt()) { // multiplier is an empirical value that depends on the 'rZ' + // 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 posXscr = FastMath.interpolateLinear(takeUniformRand(0f..1f), -hCloudSize, App.scr.width + hCloudSize) - val z = takeUniformRand(1f..ALPHA_ROLLOFF_Z/4f).pow(1.5f) // clouds are more likely to spawn with low Z-value + val z = takeUniformRand(0.1f..ALPHA_ROLLOFF_Z - 0.1f).pow(1.5f) // clouds are more likely to spawn with low Z-value val x = WeatherObjectCloud.screenXtoWorldX(posXscr, z) tryToSpawnCloud(currentWeather, Vector3(x, 0f, z)) @@ -397,6 +477,7 @@ internal object WeatherMixer : RNGConsumer { internal fun titleScreenInitWeather() { currentWeather = weatherList["titlescreen"]!![0] + currentWeather.forceWindVec = Vector3(-0.98f, 0f, 0.21f) initClouds() } @@ -687,6 +768,7 @@ internal object WeatherMixer : RNGConsumer { classification = classification, cloudChance = JSON.getFloat("cloudChance"), windSpeed = JSON.getFloat("windSpeed"), + windSpeedVariance = JSON.getFloat("windSpeedVariance"), cloudGamma = JSON["cloudGamma"].asFloatArray().let { Vector2(it[0], it[1]) }, cloudGammaVariance = JSON["cloudGammaVariance"].asFloatArray().let { Vector2(it[0], it[1]) }, clouds = cloudsMap, diff --git a/src/net/torvald/terrarum/weather/WeatherObjectCloud.kt b/src/net/torvald/terrarum/weather/WeatherObjectCloud.kt index 576aaddcd..ab669c9ea 100644 --- a/src/net/torvald/terrarum/weather/WeatherObjectCloud.kt +++ b/src/net/torvald/terrarum/weather/WeatherObjectCloud.kt @@ -27,14 +27,14 @@ class WeatherObjectCloud(private val texture: TextureRegion, private val flipW: * FlowVector: In which direction the cloud flows. Vec3(dX, dY, dScale) * Resulting vector: (x + dX, y + dY, scale * dScale) */ - fun update(flowVector: Vector3, gait: Float) { + fun update(flowVector: Vector3) { pos.add( flowVector.cpy(). scl(1f, 1f, getZflowMult(posZ)). // this will break the perspective if flowVector.z.abs() is close to 1, but it has to be here to "keep the distance" - scl(vecMult).scl(gait) + scl(vecMult) ) - alpha = if (posZ < 1f) posZ.pow(0.5f) else -(posZ / ALPHA_ROLLOFF_Z) + 1f + alpha = if (posZ < 1f) posZ.pow(0.5f) else -((posZ - 1f) / ALPHA_ROLLOFF_Z) + 1f val lrCoord = screenCoordBottomLRforDespawnCalculation if (lrCoord.x > WeatherMixer.oobMarginR || lrCoord.z < WeatherMixer.oobMarginL || posZ !in 0.0001f..ALPHA_ROLLOFF_Z + 1f || alpha < 0f) { @@ -47,7 +47,7 @@ class WeatherObjectCloud(private val texture: TextureRegion, private val flipW: private val w = App.scr.halfwf private val h = App.scr.hf * 0.5f - private val vecMult = Vector3(1f, 1f, 1f / (2f * h)) + private val vecMult = Vector3(1f, 1f, 1f / (4f * h)) /** * X/Y position is a bottom-centre point of the image