randomised weather but i'm just faking it rn

This commit is contained in:
minjaesong
2023-08-23 18:12:32 +09:00
parent 8535b0ce13
commit bf87dc04cb
5 changed files with 168 additions and 61 deletions

View File

@@ -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,

View File

@@ -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) {}

View File

@@ -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<CloudProps>, // 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<CloudProps>, // 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
}
}

View File

@@ -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<WeatherObjectCloud>()
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<Float>, random: Float) =
((range.start + range.endInclusive) / 2f) + random * (range.endInclusive - range.start) / 2f
private fun takeUniformRand(range: ClosedFloatingPointRange<Float>) =
FastMath.interpolateLinear(Math.random().toFloat(), range.start, range.endInclusive)
private fun takeTriangularRand(range: ClosedFloatingPointRange<Float>) =
@@ -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,

View File

@@ -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