more versatile weather CLUT defs

This commit is contained in:
minjaesong
2025-03-22 19:39:35 +09:00
parent c54cafae0f
commit 5c2d201151
15 changed files with 176 additions and 72 deletions

View File

@@ -13,14 +13,11 @@ import net.torvald.unicode.EMDASH
import net.torvald.colourutil.*
import net.torvald.parametricsky.datasets.DatasetCIEXYZ
import net.torvald.terrarum.abs
import net.torvald.terrarum.clut.Skybox
import net.torvald.terrarum.clut.Skybox.coerceInSmoothly
import net.torvald.terrarum.clut.Skybox.mapCircle
import net.torvald.terrarum.inUse
import net.torvald.terrarum.modulebasegame.worldgenerator.HALF_PI
import net.torvald.terrarum.weather.SkyboxModelHosek.coerceInSmoothly
import java.awt.BorderLayout
import java.awt.Dimension
import java.lang.Math.pow
import javax.swing.*
import kotlin.math.*

View File

@@ -61,6 +61,15 @@ class GdxColorMap {
is2D = (height > 1)
}
constructor(width: Int, height: Int, colours: List<Color>) {
dataRaw = colours.map { it.toIntBits() }.toIntArray()
dataGdxColor = dataRaw.map { Color(it) }.toTypedArray()
dataCvec = dataRaw.map { Cvec(it) }.toTypedArray()
this.width = width
this.height = height
is2D = (height > 1)
}
private val dataRaw: IntArray
private val dataGdxColor: Array<Color>
private val dataCvec: Array<Cvec>

View File

@@ -8,12 +8,13 @@ import net.torvald.colourutil.toRGB
import net.torvald.gdx.graphics.Cvec
import net.torvald.parametricsky.ArHosekSkyModel
import net.torvald.terrarum.abs
import net.torvald.terrarum.clut.Skybox.coerceInSmoothly
import net.torvald.terrarum.clut.Skybox.mapCircle
import net.torvald.terrarum.clut.Skybox.scaleToFit
import net.torvald.terrarum.modulebasegame.worldgenerator.HALF_PI
import net.torvald.terrarum.serialise.toLittle
import net.torvald.terrarum.serialise.toUint
import net.torvald.terrarum.weather.SkyboxModelHosek
import net.torvald.terrarum.weather.SkyboxModelHosek.coerceInSmoothly
import net.torvald.terrarum.weather.SkyboxModelHosek.mapCircle
import net.torvald.terrarum.weather.SkyboxModelHosek.scaleToFit
import java.io.File
import kotlin.math.PI
import kotlin.math.cos
@@ -42,7 +43,7 @@ class GenerateSkyboxTextureAtlas {
val state =
ArHosekSkyModel.arhosek_xyz_skymodelstate_alloc_init(turbidity, albedo, elevationRad.abs())
for (yp in 0 until Skybox.gradSize) {
for (yp in 0 until SkyboxModelHosek.gradSize) {
val yi = yp - 10
val xf = -elevationDeg / 90.0
var yf = (yi / 58.0).coerceIn(0.0, 1.0).mapCircle().coerceInSmoothly(0.0, 0.95)
@@ -81,9 +82,9 @@ class GenerateSkyboxTextureAtlas {
// y: increasing turbidity (1.0 .. 10.0, in steps of 0.333)
// x: elevations (-75 .. 75 in steps of 1, then albedo of [0.1, 0.3, 0.5, 0.7, 0.9])
val TGA_HEADER_SIZE = 18
val texh = Skybox.gradSize * Skybox.turbCnt
val texh2 = Skybox.turbCnt
val texw = Skybox.elevCnt * Skybox.albedoCnt * 2
val texh = SkyboxModelHosek.gradSize * SkyboxModelHosek.turbCnt
val texh2 = SkyboxModelHosek.turbCnt
val texw = SkyboxModelHosek.elevCnt * SkyboxModelHosek.albedoCnt * 2
val bytesSize = texw * texh
val bytes2Size = texw * texh2
val bytes = ByteArray(TGA_HEADER_SIZE + bytesSize * 4 + 26)
@@ -113,18 +114,18 @@ class GenerateSkyboxTextureAtlas {
// write pixels
for (gammaPair in 0..1) {
for (albedo0 in 0 until Skybox.albedoCnt) {
val albedo = Skybox.albedos[albedo0]
for (albedo0 in 0 until SkyboxModelHosek.albedoCnt) {
val albedo = SkyboxModelHosek.albedos[albedo0]
println("Albedo=$albedo")
for (turb0 in 0 until Skybox.turbCnt) {
val turbidity = Skybox.turbiditiesD[turb0]
for (turb0 in 0 until SkyboxModelHosek.turbCnt) {
val turbidity = SkyboxModelHosek.turbiditiesD[turb0]
println("....... Turbidity=$turbidity")
for (elev0 in 0 until Skybox.elevCnt) {
var elevationDeg = Skybox.elevationsD[elev0]
for (elev0 in 0 until SkyboxModelHosek.elevCnt) {
var elevationDeg = SkyboxModelHosek.elevationsD[elev0]
if (elevationDeg == 0.0) elevationDeg = 0.5 // dealing with the edge case
generateStrip(gammaPair, albedo, turbidity, elevationDeg) { yp, i, colour ->
val imgOffX = albedo0 * Skybox.elevCnt + elev0 + Skybox.elevCnt * Skybox.albedoCnt * gammaPair
val imgOffY = texh - 1 - (Skybox.gradSize * turb0 + yp)
val imgOffX = albedo0 * SkyboxModelHosek.elevCnt + elev0 + SkyboxModelHosek.elevCnt * SkyboxModelHosek.albedoCnt * gammaPair
val imgOffY = texh - 1 - (SkyboxModelHosek.gradSize * turb0 + yp)
val fileOffset = TGA_HEADER_SIZE + 4 * (imgOffY * texw + imgOffX)
bytes[fileOffset + i] = colour
}
@@ -140,8 +141,8 @@ class GenerateSkyboxTextureAtlas {
private val gradSizes = listOf(50)
private fun getByte(gammaPair: Int, albedo0: Int, turb0: Int, elev0: Int, yp: Int, channel: Int): Byte {
val imgOffX = albedo0 * Skybox.elevCnt + elev0 + Skybox.elevCnt * Skybox.albedoCnt * gammaPair
val imgOffY = texh - 1 - (Skybox.gradSize * turb0 + yp)
val imgOffX = albedo0 * SkyboxModelHosek.elevCnt + elev0 + SkyboxModelHosek.elevCnt * SkyboxModelHosek.albedoCnt * gammaPair
val imgOffY = texh - 1 - (SkyboxModelHosek.gradSize * turb0 + yp)
val fileOffset = TGA_HEADER_SIZE + 4 * (imgOffY * texw + imgOffX)
return bytes[fileOffset + channel]
}
@@ -171,13 +172,13 @@ class GenerateSkyboxTextureAtlas {
for (gammaPair in 0..1) {
for (albedo0 in 0 until Skybox.albedoCnt) {
val albedo = Skybox.albedos[albedo0]
for (albedo0 in 0 until SkyboxModelHosek.albedoCnt) {
val albedo = SkyboxModelHosek.albedos[albedo0]
println("Albedo=$albedo")
for (turb0 in 0 until Skybox.turbCnt) {
val turbidity = Skybox.turbiditiesD[turb0]
for (turb0 in 0 until SkyboxModelHosek.turbCnt) {
val turbidity = SkyboxModelHosek.turbiditiesD[turb0]
println("....... Turbidity=$turbidity")
for (elev0 in 0 until Skybox.elevCnt) {
for (elev0 in 0 until SkyboxModelHosek.elevCnt) {
val avrB = (gradSizes.sumOf { getByte(gammaPair, albedo0, turb0, elev0, it, 0).toUint() }.toDouble() / gradSizes.size).div(255.0).toFloat()
val avrG = (gradSizes.sumOf { getByte(gammaPair, albedo0, turb0, elev0, it, 1).toUint() }.toDouble() / gradSizes.size).div(255.0).toFloat()
@@ -193,7 +194,7 @@ class GenerateSkyboxTextureAtlas {
colour.a.times(255f).roundToInt().coerceIn(0..255).toByte()
)
val imgOffX = albedo0 * Skybox.elevCnt + elev0 + Skybox.elevCnt * Skybox.albedoCnt * gammaPair
val imgOffX = albedo0 * SkyboxModelHosek.elevCnt + elev0 + SkyboxModelHosek.elevCnt * SkyboxModelHosek.albedoCnt * gammaPair
val imgOffY = texh2 - 1 - turb0
val fileOffset = TGA_HEADER_SIZE + 4 * (imgOffY * texw + imgOffX)

View File

@@ -17,7 +17,6 @@ import net.torvald.terrarum.App.printdbgerr
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZED
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZEF
import net.torvald.terrarum.clut.Skybox
import net.torvald.terrarum.console.CommandDict
import net.torvald.terrarum.gameactors.*
import net.torvald.terrarum.gameactors.ai.ActorAI
@@ -35,8 +34,8 @@ import net.torvald.terrarum.realestate.LandUtil
import net.torvald.terrarum.serialise.ReadSimpleWorld
import net.torvald.terrarum.ui.Toolkit
import net.torvald.terrarum.ui.UICanvas
import net.torvald.terrarum.ui.UIItemTextButton
import net.torvald.terrarum.utils.OpenURL
import net.torvald.terrarum.weather.SkyboxModelHosek
import net.torvald.terrarum.weather.WeatherMixer
import net.torvald.terrarum.worlddrawer.WorldCamera
import net.torvald.util.CircularArray
@@ -247,7 +246,7 @@ class TitleScreen(batch: FlippingSpriteBatch) : IngameInstance(batch) {
uiContainer.add(uiRemoCon)
CommandDict // invoke
Skybox.loadlut() // invoke
SkyboxModelHosek.loadlut() // invoke
// Skybox.initiate() // invoke the lengthy calculation
// TODO add console here

View File

@@ -19,7 +19,7 @@ import kotlin.math.absoluteValue
data class BaseModularWeather(
val identifier: String,
val json: JsonValue,
var skyboxGradColourMap: GdxColorMap, // row 0: skybox grad top, row 1: skybox grad bottom, row 2: sunlight (RGBA)
var skyboxGradColourMap: SkyboxModel, // row 0: skybox grad top, row 1: skybox grad bottom, row 2: sunlight (RGBA)
val daylightClut: GdxColorMap,
val tags: List<String>,
val cloudChance: Float,

View File

@@ -0,0 +1,12 @@
package net.torvald.terrarum.weather
import net.torvald.terrarum.GdxColorMap
/**
* Created by minjaesong on 2025-03-22.
*/
class SkyboxGradSimple(val clut: GdxColorMap): SkyboxModel {
override fun dispose() {}
}

View File

@@ -0,0 +1,9 @@
package net.torvald.terrarum.weather
import com.badlogic.gdx.utils.Disposable
/**
* Created by minjaesong on 2025-03-22.
*/
interface SkyboxModel : Disposable {
}

View File

@@ -1,4 +1,4 @@
package net.torvald.terrarum.clut
package net.torvald.terrarum.weather
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Pixmap
@@ -14,14 +14,13 @@ import net.torvald.terrarum.App
import net.torvald.terrarum.App.printdbg
import net.torvald.terrarum.abs
import net.torvald.terrarum.floorToInt
import net.torvald.terrarum.toInt
import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack
import kotlin.math.*
/**
* Created by minjaesong on 2023-07-09.
*/
object Skybox : Disposable {
object SkyboxModelHosek : SkyboxModel {
private const val HALF_PI = 1.5707963267948966
private const val PI = 3.141592653589793
@@ -294,8 +293,8 @@ object Skybox : Disposable {
}
override fun dispose() {
if (Skybox::gradTexBinLowAlbedo.isInitialized) gradTexBinLowAlbedo.forEach { it.texture.dispose() }
if (Skybox::gradTexBinHighAlbedo.isInitialized) gradTexBinHighAlbedo.forEach { it.texture.dispose() }
if (Skybox::tex.isInitialized) tex.dispose()
if (SkyboxModelHosek::gradTexBinLowAlbedo.isInitialized) gradTexBinLowAlbedo.forEach { it.texture.dispose() }
if (SkyboxModelHosek::gradTexBinHighAlbedo.isInitialized) gradTexBinHighAlbedo.forEach { it.texture.dispose() }
if (SkyboxModelHosek::tex.isInitialized) tex.dispose()
}
}

View File

@@ -1,5 +1,6 @@
package net.torvald.terrarum.weather
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.utils.Disposable
import net.torvald.terrarum.App
@@ -39,6 +40,8 @@ class WeatherCodex : Disposable {
fun readFromJson(modname: String, file: File) = readFromJson(modname, file.path)
private val pathToImage = "weathers"
fun readFromJson(modname: String, path: String) {
/* JSON structure:
{
@@ -52,12 +55,11 @@ class WeatherCodex : Disposable {
]
}
*/
val pathToImage = "weathers"
val JSON = JsonFetcher(path)
val skyboxInJson = JSON.getString("skyboxGradColourMap")
val lightbox = JSON.getString("daylightClut")
val skyboxModel = JSON.getString("skyboxGradColourMap")
val lightboxModel = JSON.getString("daylightClut")
val cloudsMap = ArrayList<CloudProps>()
val clouds = JSON["clouds"]
@@ -81,8 +83,8 @@ class WeatherCodex : Disposable {
val obj = BaseModularWeather(
identifier = ident,
json = JSON,
skyboxGradColourMap = GdxColorMap(ModMgr.getGdxFile(modname, "$pathToImage/${skyboxInJson}")),
daylightClut = GdxColorMap(ModMgr.getGdxFile(modname, "$pathToImage/${lightbox}")),
skyboxGradColourMap = getSkyboxModelByName(modname, skyboxModel),
daylightClut = getLightboxModelByName(modname, lightboxModel),
tags = tags,
cloudChance = JSON.getFloat("cloudChance"),
windSpeed = JSON.getFloat("windSpeed"),
@@ -118,4 +120,71 @@ class WeatherCodex : Disposable {
}
else getByTag(tag)!!.random()
}
private fun getSkyboxModelByName(modname: String, name: String): SkyboxModel {
return if (name.startsWith("model:")) {
when (name.substring(6)) {
"hosek" -> SkyboxModelHosek
else -> throw UnsupportedOperationException("Unknown skybox model: '$name'")
}
}
else if (name.startsWith("lut:")) {
val filename = name.substring(4)
val colourMap = GdxColorMap(ModMgr.getGdxFile(modname, "$pathToImage/${filename}"))
SkyboxGradSimple(colourMap)
}
else if (name.startsWith("static:")) {
val argstr = name.substring(7)
val args = argstr.split(',').map {
if (it.length == 7) // #RRGGBB
Color(
it.substring(1, 3).toInt(16) / 255.0f,
it.substring(3, 5).toInt(16) / 255.0f,
it.substring(5, 7).toInt(16) / 255.0f,
1f,
)
else if (it.length == 9) // #RRGGBBAA
Color(
it.substring(1, 3).toInt(16) / 255.0f,
it.substring(3, 5).toInt(16) / 255.0f,
it.substring(5, 7).toInt(16) / 255.0f,
it.substring(7, 9).toInt(16) / 255.0f,
)
else if (it.length == 4) // #RGB
Color(
it.substring(1, 2).toInt(16) / 15.0f,
it.substring(2, 3).toInt(16) / 15.0f,
it.substring(3, 4).toInt(16) / 15.0f,
1f,
)
else if (it.length == 5) // #RGBA
Color(
it.substring(1, 2).toInt(16) / 15.0f,
it.substring(2, 3).toInt(16) / 15.0f,
it.substring(3, 4).toInt(16) / 15.0f,
it.substring(4, 5).toInt(16) / 15.0f,
)
else throw IllegalArgumentException("Unknown colour code: $it")
}
SkyboxGradSimple(GdxColorMap(args.size, 1, args))
}
else {
throw UnsupportedOperationException("Unknown skybox: '$name'")
}
}
private fun getLightboxModelByName(modname: String, name: String): GdxColorMap {
return if (name.startsWith("lut:")) {
val filename = name.substring(4)
GdxColorMap(ModMgr.getGdxFile(modname, "$pathToImage/${filename}"))
}
else if (name.startsWith("static:")) {
val argstr = name.substring(7)
val args = argstr.split(',').map { Color.WHITE }
GdxColorMap(args.size, 1, args)
}
else {
throw UnsupportedOperationException("Unknown skybox: '$name'")
}
}
}

View File

@@ -19,17 +19,11 @@ 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.RNGConsumer
import net.torvald.terrarum.clut.Skybox
import net.torvald.terrarum.clut.Skybox.elevCnt
import net.torvald.terrarum.utils.JsonFetcher
import net.torvald.terrarum.utils.forEachSiblings
import net.torvald.terrarum.weather.SkyboxModelHosek.elevCnt
import net.torvald.terrarum.weather.WeatherObjectCloud.Companion.ALPHA_ROLLOFF_Z
import net.torvald.terrarum.weather.WeatherObjectCloud.Companion.NEWBORN_GROWTH_TIME
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
import java.lang.Math.toDegrees
import kotlin.collections.ArrayList
@@ -57,7 +51,7 @@ internal object WeatherMixer : RNGConsumer {
val DEFAULT_WEATHER = BaseModularWeather(
"default",
JsonValue(JsonValue.ValueType.`object`),
GdxColorMap(1, 3, Color(0x55aaffff), Color(0xaaffffff.toInt()), Color.WHITE),
SkyboxGradSimple(GdxColorMap(1, 2, Color(0x55aaffff), Color(0xaaffffff.toInt()))),
GdxColorMap(2, 2, Color.WHITE, Color.WHITE, Color.WHITE, Color.WHITE),
listOf("default"),
0f,
@@ -556,7 +550,7 @@ internal object WeatherMixer : RNGConsumer {
fun g(x: Double) = Math.toDegrees(atan(RECIPROCAL_OF_APPARENT_SOLAR_Y_AT_45DEG * x))
val phi = currentsolarDeg + CLOUD_SOLARDEG_OFFSET
val x = cloudY
return g(x + a(phi)).bipolarClamp(Skybox.elevMax)
return g(x + a(phi)).bipolarClamp(SkyboxModelHosek.elevMax)
}
/**
@@ -661,7 +655,7 @@ internal object WeatherMixer : RNGConsumer {
val gradY = -(gH - App.scr.height) * ((parallax + 1f) / 2f)
val (tex, uvs, turbTihsBlend, albThisBlend, turbOldBlend, albOldBlend) = Skybox.getUV(
val (tex, uvs, turbTihsBlend, albThisBlend, turbOldBlend, albOldBlend) = SkyboxModelHosek.getUV(
solarElev,
oldTurbidity,
oldAlbedo,
@@ -819,14 +813,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(SkyboxModelHosek.turbiditiesD.first(), SkyboxModelHosek.turbiditiesD.last()).minus(1.0)
.times(SkyboxModelHosek.turbDivisor)
val turbY1 = turbY.floorToInt()
val turbY2 = (turbY1 + 1).coerceAtMost(Skybox.turbCnt - 1)
val turbY2 = (turbY1 + 1).coerceAtMost(SkyboxModelHosek.turbCnt - 1)
val tx = turbY - turbY1
// coarse-grained
val albX =
albedo.coerceIn(Skybox.albedos.first(), Skybox.albedos.last()).times(5.0) // 0..5
albedo.coerceIn(SkyboxModelHosek.albedos.first(), SkyboxModelHosek.albedos.last()).times(5.0) // 0..5
val albX1 = albX.floorToInt() * elevCnt
val albX2 = (albX + 1).floorToInt().coerceAtMost(5) * elevCnt
val bx = ((albX * elevCnt) - albX1) / elevCnt
@@ -844,14 +838,14 @@ internal object WeatherMixer : RNGConsumer {
val a2t1b2A = colorMap.getCvec(albX2 + angleX2, turbY1)
val a1t2b2A = colorMap.getCvec(albX2 + angleX1, turbY2)
val a2t2b2A = colorMap.getCvec(albX2 + angleX2, turbY2)
val a1t1b1B = colorMap.getCvec(albX1 + angleX1 + Skybox.albedoCnt * elevCnt, turbY1)
val a2t1b1B = colorMap.getCvec(albX1 + angleX2 + Skybox.albedoCnt * elevCnt, turbY1)
val a1t2b1B = colorMap.getCvec(albX1 + angleX1 + Skybox.albedoCnt * elevCnt, turbY2)
val a2t2b1B = colorMap.getCvec(albX1 + angleX2 + Skybox.albedoCnt * elevCnt, turbY2)
val a1t1b2B = colorMap.getCvec(albX2 + angleX1 + Skybox.albedoCnt * elevCnt, turbY1)
val a2t1b2B = colorMap.getCvec(albX2 + angleX2 + Skybox.albedoCnt * elevCnt, turbY1)
val a1t2b2B = colorMap.getCvec(albX2 + angleX1 + Skybox.albedoCnt * elevCnt, turbY2)
val a2t2b2B = colorMap.getCvec(albX2 + angleX2 + Skybox.albedoCnt * elevCnt, turbY2)
val a1t1b1B = colorMap.getCvec(albX1 + angleX1 + SkyboxModelHosek.albedoCnt * elevCnt, turbY1)
val a2t1b1B = colorMap.getCvec(albX1 + angleX2 + SkyboxModelHosek.albedoCnt * elevCnt, turbY1)
val a1t2b1B = colorMap.getCvec(albX1 + angleX1 + SkyboxModelHosek.albedoCnt * elevCnt, turbY2)
val a2t2b1B = colorMap.getCvec(albX1 + angleX2 + SkyboxModelHosek.albedoCnt * elevCnt, turbY2)
val a1t1b2B = colorMap.getCvec(albX2 + angleX1 + SkyboxModelHosek.albedoCnt * elevCnt, turbY1)
val a2t1b2B = colorMap.getCvec(albX2 + angleX2 + SkyboxModelHosek.albedoCnt * elevCnt, turbY1)
val a1t2b2B = colorMap.getCvec(albX2 + angleX1 + SkyboxModelHosek.albedoCnt * elevCnt, turbY2)
val a2t2b2B = colorMap.getCvec(albX2 + angleX2 + SkyboxModelHosek.albedoCnt * elevCnt, turbY2)
// no srgblerp here to match the skybox shader's behaviour