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

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

@@ -1,301 +0,0 @@
package net.torvald.terrarum.clut
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Pixmap
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.TextureRegion
import com.badlogic.gdx.utils.Disposable
import com.jme3.math.FastMath
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.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 {
private const val HALF_PI = 1.5707963267948966
private const val PI = 3.141592653589793
private const val TWO_PI = 6.283185307179586
const val gradSize = 78
private lateinit var gradTexBinLowAlbedo: Array<TextureRegion>
private lateinit var gradTexBinHighAlbedo: Array<TextureRegion>
private lateinit var tex: Texture
private lateinit var texRegions: TextureRegionPack
private lateinit var texStripRegions: TextureRegionPack
fun loadlut() {
tex = Texture(Gdx.files.internal("assets/clut/skybox.png"))
tex.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear)
tex.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat)
texRegions = TextureRegionPack(tex, 2, gradSize - 2, 0, 2, 0, 1)
texStripRegions = TextureRegionPack(tex, elevCnt, gradSize - 2, 0, 2, 0, 1)
}
// use internal LUT
/*operator fun get(elevationDeg: Double, turbidity: Double, albedo: Double): TextureRegion {
val elev = elevationDeg.coerceIn(-elevBias, elevBias).times(2.0).roundToInt().plus(150)
val turb = turbidity.coerceIn(1.0, 10.0).minus(1.0).times(turbDivisor).roundToInt()
val alb = albedo.coerceIn(0.1, 0.9).minus(0.1).times(turbDivisor).roundToInt()
return gradTexBinLowAlbedo[elev * turbCnt + turb]
}*/
// use external LUT
operator fun get(elevationDeg: Double, turbidity: Double, albedo: Double, isAfternoon: Boolean): TextureRegion {
TODO()
}
data class SkyboxRenderInfo(
val texture: Texture,
val uvs: FloatArray,
val turbidityThisBlend: Float,
val albedoThisBlend: Float,
val turbidityOldBlend: Float,
val albedoOldBlend: Float,
)
fun getUV(elevationDeg: Double, oldTurbidity: Double, oldAlbedo: Double, thisTurbidity: Double, thisAlbedo: Double): SkyboxRenderInfo {
val turb2 = thisTurbidity.coerceIn(turbiditiesD.first(), turbiditiesD.last()).minus(1.0).times(turbDivisor)
val turb2Lo = turb2.floorToInt()
val turb2Hi = min(turbCnt - 1, turb2Lo + 1)
val alb2 = thisAlbedo.coerceIn(albedos.first(), albedos.last()).times(5.0)
val alb2Lo = alb2.floorToInt()
val alb2Hi = min(albedoCnt - 1, alb2Lo + 1)
val turb1 = oldTurbidity.coerceIn(turbiditiesD.first(), turbiditiesD.last()).minus(1.0).times(turbDivisor)
val turb1Lo = turb1.floorToInt()
val turb1Hi = min(turbCnt - 1, turb1Lo + 1)
val alb1 = oldAlbedo.coerceIn(albedos.first(), albedos.last()).times(5.0)
val alb1Lo = alb1.floorToInt()
val alb1Hi = min(albedoCnt - 1, alb1Lo + 1)
val elev = elevationDeg.coerceIn(-elevMax, elevMax).plus(elevMax).div(elevations.last.toDouble()).div(albedoCnt * 2).times((elevCnt - 1.0) / elevCnt)
// A: morn, turbLow, albLow
// B: noon, turbLow, albLow
// C: morn, turbHigh, albLow
// D: noon, turbHigh, albLow
// E: morn, turbLow, albHigh
// F: noon, turbLow, albHigh
// G: morn, turbHigh, albHigh
// H: noon, turbHigh, albHigh
val regionA = texStripRegions.get(alb1Lo, turb1Lo)
val regionB = texStripRegions.get(alb2Lo, turb2Lo)
val regionC = texStripRegions.get(alb1Lo, turb1Hi)
val regionD = texStripRegions.get(alb2Lo, turb2Hi)
val regionE = texStripRegions.get(alb1Hi, turb1Lo)
val regionF = texStripRegions.get(alb2Hi, turb2Lo)
val regionG = texStripRegions.get(alb1Hi, turb1Hi)
val regionH = texStripRegions.get(alb2Hi, turb2Hi)
// (0.5f / tex.width): because of the nature of bilinear interpolation, half pixels from the edges must be discarded
val uA = regionA.u + (0.5f / tex.width) + elev.toFloat()
val uB = regionB.u + (0.5f / tex.width) + elev.toFloat()
val uC = regionC.u + (0.5f / tex.width) + elev.toFloat()
val uD = regionD.u + (0.5f / tex.width) + elev.toFloat()
val uE = regionE.u + (0.5f / tex.width) + elev.toFloat()
val uF = regionF.u + (0.5f / tex.width) + elev.toFloat()
val uG = regionG.u + (0.5f / tex.width) + elev.toFloat()
val uH = regionH.u + (0.5f / tex.width) + elev.toFloat()
return SkyboxRenderInfo(
tex,
floatArrayOf(
uA, regionA.v, uA, regionA.v2,
uB, regionB.v, uB, regionB.v2,
uC, regionC.v, uC, regionC.v2,
uD, regionD.v, uD, regionD.v2,
uE, regionE.v, uE, regionE.v2,
uF, regionF.v, uF, regionF.v2,
uG, regionG.v, uG, regionG.v2,
uH, regionH.v, uH, regionH.v2,
),
(turb2 - turb2Lo).toFloat(),
(alb2 - alb2Lo).toFloat(),
(turb1 - turb1Lo).toFloat(),
(alb1 - alb1Lo).toFloat(),
)
}
private fun Float.scaleFun() =
(1f - 1f / 2f.pow(this/6f)) * 0.97f
internal fun CIEXYZ.scaleToFit(elevationDeg: Double): CIEXYZ {
return if (elevationDeg >= 0) {
CIEXYZ(
this.X.scaleFun(),
this.Y.scaleFun(),
this.Z.scaleFun(),
this.alpha
)
}
else {
// maths model: https://www.desmos.com/calculator/cwi7iyzygg
val x = -elevationDeg.toFloat()
// val elevation2 = elevationDeg.toFloat() / 28.5f
val p = 3.5f
val q = 7.5f
val s = -0.2f
val f = (1f - (1f - 1f / 1.8f.pow(x)) * 0.97f).toFloat()
// val g = (1.0 - (elevation2.pow(E) / E.pow(elevation2))*0.8).toFloat()
val h = ((x / q).pow(p) + 1f).pow(s)
CIEXYZ(
this.X.scaleFun() * f * h,
this.Y.scaleFun() * f * h,
this.Z.scaleFun() * f * h,
this.alpha
)
}
}
val elevations = (0..150)
val elevMax = elevations.last / 2.0
val elevationsD = elevations.map { -elevMax + it } // -75, -74, -73, ..., 74, 75 // (specifically using whole number of angles because angle units any finer than 1.0 would make "hack" sunsut happen too fast)
val turbidities = (0..45) // 1, 1.2, 1.4, 1.6, ..., 10.0
val turbDivisor = 5.0
val turbiditiesD = turbidities.map { 1.0 + it / turbDivisor }
val albedos = arrayOf(0.0, 0.2, 0.4, 0.6, 0.8, 1.0)
val elevCnt = elevations.count()
val turbCnt = turbidities.count()
val albedoCnt = albedos.size
val gamma = HALF_PI
internal fun Double.mapCircle() = sin(HALF_PI * this)
internal fun initiate() {
printdbg(this, "Initialising skybox model")
TODO()
App.disposables.add(this)
printdbg(this, "Skybox model generated!")
}
/**
* See https://www.desmos.com/calculator/lcvvsju3p1 for mathematical definition
* @param p decay point. 0.0..1.0
* @param q polynomial degree. 2+. Larger value means sharper transition around the point p
* @param x the 'x' value of the function, as in `y=f(x)`. 0.0..1.0
*/
internal fun polynomialDecay(p: Double, q: Int, x: Double): Double {
val sign = if (q % 2 == 1) -1 else 1
val a1 = -1.0 / p
val a2 = 1.0 / (1.0 - p)
val q = q.toDouble()
return if (x < p)
sign * a1.pow(q - 1.0) * x.pow(q) + 1.0
else
sign * a2.pow(q - 1.0) * (x - 1.0).pow(q)
}
internal fun polynomialDecay2(p: Double, q: Int, x: Double): Double {
val sign = if (q % 2 == 1) 1 else -1
val a1 = -1.0 / p
val a2 = 1.0 / (1.0 - p)
val q = q.toDouble()
return if (x < p)
sign * a1.pow(q - 1.0) * x.pow(q)
else
sign * a2.pow(q - 1.0) * (x - 1.0).pow(q) + 1.0
}
internal fun superellipsoidDecay(p: Double, x: Double): Double {
return 1.0 - (1.0 - (1.0 - x).pow(1.0 / p)).pow(p)
}
internal fun Double.coerceInSmoothly(low: Double, high: Double): Double {
val x = this.coerceIn(low, high)
val x2 = ((x - low) * (high - low).pow(-1.0))
// return FastMath.interpolateLinear(polynomialDecay2(0.5, 2, x2), low, high)
return FastMath.interpolateLinear(smoothLinear(0.2, x2), low, high)
}
/**
* To get the idea what the fuck is going on here, please refer to https://www.desmos.com/calculator/snqglcu2wl
*
* Sidenote: the original model involved two cosine curves, but since its Taylor series begins with x^2, I figured
* quadratic curve ought to be good enough, and the error against the original model was below 1/255 for
* reasonable range of p, and that's the reason I stopped at x^2 rather than also taking x^4 into the approximated
* model that is the code below.
*/
internal fun smoothLinear(p: Double, x0: Double): Double {
val x = x0 - 0.5
val p1 = sqrt(1.0 - 2.0 * p)
val t = 0.5 * p1
val y0 = if (x < -t)
(1.0 / p) * (x + 0.5).pow(2) - 0.5
else if (x > t)
-(1.0 / p) * (x - 0.5).pow(2) + 0.5
else
x * 2.0 / (1.0 + p1)
return y0 + 0.5
}
private fun getTexturmaps(albedo: Double): Array<TextureRegion> {
return Array(elevCnt * turbCnt) {
val elevationDeg = elevationsD[it / turbCnt]
val elevationRad = Math.toRadians(elevationDeg)
val turbidity = turbiditiesD[it % turbCnt]
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 (yp in 0 until 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)
// experiments visualisation: https://www.desmos.com/calculator/5crifaekwa
// if (elevationDeg < 0) yf *= 1.0 - pow(xf, 0.333)
// if (elevationDeg < 0) yf *= -2.0 * asin(xf - 1.0) / PI
if (elevationDeg < 0) yf *= superellipsoidDecay(1.0 / 3.0, xf)
val theta = yf * HALF_PI
// vertical angle, where 0 is zenith, ±90 is ground (which is odd)
// println("$yp\t$theta")
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(if (yp in 17 until 17 + 94) Color.LIME else Color.CORAL)
pixmap.setColor(rgb)
pixmap.drawPixel(0, yp)
}
val texture = Texture(pixmap).also {
it.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear)
}
pixmap.dispose()
TextureRegion(texture)
}
}
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()
}
}