Files
Terrarum/src/net/torvald/terrarum/clut/GenerateSkyboxTextureAtlas.kt
2023-10-03 02:48:04 +09:00

244 lines
9.9 KiB
Kotlin

package net.torvald.terrarum.clut
import com.badlogic.gdx.graphics.Color
import net.torvald.colourutil.CIEXYZ
import net.torvald.colourutil.HUSLColorConverter
import net.torvald.colourutil.toColor
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 java.io.File
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.roundToInt
/**
* Created by minjaesong on 2023-08-01.
*/
class GenerateSkyboxTextureAtlas {
fun generateStrip(
gammaPair: Int,
albedo: Double,
turbidity: Double,
elevationDeg: Double,
writefun: (Int, Int, Byte) -> Unit
) {
val elevationRad = Math.toRadians(elevationDeg)
/*val gamma = if (gammaPair == 0) HALF_PI else {
Math.toRadians(180 + 114 + 24 * cos(PI * elevationDeg / 40))
}*/
val gamma = Math.toRadians(115 + 25 * cos(PI * elevationDeg / 40)) + (gammaPair * PI)
// println("... Elevation: $elevationDeg")
val state =
ArHosekSkyModel.arhosek_xyz_skymodelstate_alloc_init(turbidity, albedo, elevationRad.abs())
for (yp in 0 until Skybox.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 *= Skybox.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().gamma(1.2f)
val colour = rgb.toIntBits().toLittle()
for (i in 0..3) {
writefun(yp, i, colour[bytesLut[i]])
}
}
}
private fun Color.gamma(gam: Float): Color {
this.r = this.r.pow(gam)
this.g = this.g.pow(gam)
this.b = this.b.pow(gam)
return this
}
// 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 bytesSize = texw * texh
val bytes2Size = texw * texh2
val bytes = ByteArray(TGA_HEADER_SIZE + bytesSize * 4 + 26)
val bytes2 = ByteArray(TGA_HEADER_SIZE + texw * bytes2Size * 4 + 26)
fun generateMainFile() {
// write header
byteArrayOf(
0, // ID field
0, // colour map (none)
2, // colour type (unmapped RGB)
0, 0, 0, 0, 0, // colour map spec (empty)
0, 0, 0, 0, // unused for modern purposes
(texw and 255).toByte(), (texw.ushr(8) and 255).toByte(), // width
(texh and 255).toByte(), (texh.ushr(8) and 255).toByte(), // height
32, // bits-per-pixel (8bpp RGBA)
8 // image descriptor (32bpp, bottom-left origin)
).forEachIndexed { i, b -> bytes[i] = b }
// write footer
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000TRUEVISION-XFILE\u002E\u0000".forEachIndexed { i, c ->
bytes[TGA_HEADER_SIZE + bytesSize * 4 + i] =
c.code.toByte()
}
println("Generating texture atlas ($texw x $texh)...")
// write pixels
for (gammaPair in 0..1) {
for (albedo0 in 0 until Skybox.albedoCnt) {
val albedo = Skybox.albedos[albedo0]
println("Albedo=$albedo")
for (turb0 in 0 until Skybox.turbCnt) {
val turbidity = Skybox.turbiditiesD[turb0]
println("....... Turbidity=$turbidity")
for (elev0 in 0 until Skybox.elevCnt) {
var elevationDeg = Skybox.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 fileOffset = TGA_HEADER_SIZE + 4 * (imgOffY * texw + imgOffX)
bytes[fileOffset + i] = colour
}
}
}
}
}
println("Atlas generation done!")
File("./assets/clut/skybox.tga").writeBytes(bytes)
}
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 fileOffset = TGA_HEADER_SIZE + 4 * (imgOffY * texw + imgOffX)
return bytes[fileOffset + channel]
}
fun generateCloudColourmap() {
if (bytes[TGA_HEADER_SIZE].toInt() == 0) throw IllegalStateException("Atlas not generated")
// write header
byteArrayOf(
0, // ID field
0, // colour map (none)
2, // colour type (unmapped RGB)
0, 0, 0, 0, 0, // colour map spec (empty)
0, 0, 0, 0, // unused for modern purposes
(texw and 255).toByte(), (texw.ushr(8) and 255).toByte(), // width
(texh2 and 255).toByte(), (texh2.ushr(8) and 255).toByte(), // height
32, // bits-per-pixel (8bpp RGBA)
8 // image descriptor (32bpp, bottom-left origin)
).forEachIndexed { i, b -> bytes2[i] = b }
// write footer
"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000TRUEVISION-XFILE\u002E\u0000".forEachIndexed { i, c ->
bytes2[TGA_HEADER_SIZE + bytes2Size * 4 + i] =
c.code.toByte()
}
println("Generating cloud colour atlas ($texw x $texh2)...")
for (gammaPair in 0..1) {
for (albedo0 in 0 until Skybox.albedoCnt) {
val albedo = Skybox.albedos[albedo0]
println("Albedo=$albedo")
for (turb0 in 0 until Skybox.turbCnt) {
val turbidity = Skybox.turbiditiesD[turb0]
println("....... Turbidity=$turbidity")
for (elev0 in 0 until Skybox.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()
val avrR = (gradSizes.sumOf { getByte(gammaPair, albedo0, turb0, elev0, it, 2).toUint() }.toDouble() / gradSizes.size).div(255.0).toFloat()
val avrA = (gradSizes.sumOf { getByte(gammaPair, albedo0, turb0, elev0, it, 3).toUint() }.toDouble() / gradSizes.size).div(255.0).toFloat()
val colour = Cvec(avrR, avrG, avrB, avrA).mul(1.02f).vibrancy(1.1f)
val colourBytes = arrayOf(
colour.b.times(255f).roundToInt().coerceIn(0..255).toByte(),
colour.g.times(255f).roundToInt().coerceIn(0..255).toByte(),
colour.r.times(255f).roundToInt().coerceIn(0..255).toByte(),
colour.a.times(255f).roundToInt().coerceIn(0..255).toByte()
)
val imgOffX = albedo0 * Skybox.elevCnt + elev0 + Skybox.elevCnt * Skybox.albedoCnt * gammaPair
val imgOffY = texh2 - 1 - turb0
val fileOffset = TGA_HEADER_SIZE + 4 * (imgOffY * texw + imgOffX)
for (i in 0..3) {
bytes2[fileOffset + i] = colourBytes[i]
}
}
}
}
}
println("Colourmap generation done!")
File("./assets/clut/skyboxavr.tga").writeBytes(bytes2)
}
fun invoke() {
generateMainFile()
generateCloudColourmap()
}
private fun Double.srgbLinearise(): Double {
return if (this > 0.0031308)
1.055 * this.pow(1 / 2.4) - 0.055
else
this * 12.92
}
private fun Cvec.vibrancy(intensity: Float): Cvec {
val luv = HUSLColorConverter.rgbToHsluv(floatArrayOf(this.r, this.g, this.b))
val sat = luv[1] // 0..100
luv[1] = (sat / 100f).pow(1f / intensity) * 100f
val rgb = HUSLColorConverter.hsluvToRgb(luv)
this.r = rgb[0]
this.g = rgb[1]
this.b = rgb[2]
return this
}
private val bytesLut = arrayOf(2, 1, 0, 3, 2, 1, 0, 3) // For some reason BGRA order is what makes it work
}
fun main() {
GenerateSkyboxTextureAtlas().invoke()
}