cloud stage has more depth

This commit is contained in:
minjaesong
2023-09-06 18:01:33 +09:00
parent 7259ca616c
commit 6c97a9d5ab
13 changed files with 93 additions and 48 deletions

View File

@@ -21,5 +21,6 @@
"altLow": 1080, "altHigh": 1800 "altLow": 1080, "altHigh": 1800
} }
}, },
"atmoTurbidity": 3.5 "atmoTurbidity": 3.5,
"shaderVibrancy": [1.0, 1.0]
} }

View File

@@ -27,5 +27,6 @@
} }
}, },
"atmoTurbidity": 3.5, "atmoTurbidity": 3.5,
"shaderVibrancy": [1.0, 1.0],
"__comment__": "Make a texture for altocumulus so that this weather can be realised with less than 1000 sprites" "__comment__": "Make a texture for altocumulus so that this weather can be realised with less than 1000 sprites"
} }

View File

@@ -8,7 +8,7 @@
"cloudGammaVariance": [0.0, 0.0], "cloudGammaVariance": [0.0, 0.0],
"windSpeed": 10.45, "windSpeed": 10.45,
"windSpeedVariance": 0.5, "windSpeedVariance": 0.5,
"windSpeedDamping": 0.0, "windSpeedDamping": 0.5,
"clouds": { "clouds": {
"cumulus": { "cumulus": {
"filename": "cloud_normal.png", "tw": 1024, "th": 512, "probability": 0.1, "filename": "cloud_normal.png", "tw": 1024, "th": 512, "probability": 0.1,
@@ -26,5 +26,6 @@
"altLow": 1100, "altHigh": 1500 "altLow": 1100, "altHigh": 1500
} }
}, },
"atmoTurbidity": 9.5 "atmoTurbidity": 9.5,
"shaderVibrancy": [0.93, 0.9]
} }

View File

@@ -13,6 +13,7 @@ import net.torvald.terrarum.App.IS_DEVELOPMENT_BUILD
import net.torvald.terrarum.gamecontroller.KeyToggler import net.torvald.terrarum.gamecontroller.KeyToggler
import net.torvald.terrarum.ui.BasicDebugInfoWindow import net.torvald.terrarum.ui.BasicDebugInfoWindow
import net.torvald.terrarum.ui.Toolkit import net.torvald.terrarum.ui.Toolkit
import net.torvald.terrarum.weather.WeatherMixer
/** /**
* Must be called by the App Loader * Must be called by the App Loader
@@ -236,6 +237,13 @@ object TerrarumPostProcessor : Disposable {
else else
shaderPostNoDither shaderPostNoDither
val (vo, vg) = INGAME.world.weatherbox.let {
if (it.currentWeather.identifier == "titlescreen")
1f to 1f
else
it.currentVibrancy.x to it.currentVibrancy.y
}
App.getCurrentDitherTex().bind(1) App.getCurrentDitherTex().bind(1)
fbo.colorBufferTexture.bind(0) fbo.colorBufferTexture.bind(0)
@@ -245,6 +253,7 @@ object TerrarumPostProcessor : Disposable {
shader.setUniformi("rnd", rng.nextInt(8192), rng.nextInt(8192)) shader.setUniformi("rnd", rng.nextInt(8192), rng.nextInt(8192))
shader.setUniformi("u_pattern", 1) shader.setUniformi("u_pattern", 1)
shader.setUniformf("quant", shaderQuant[App.getConfigInt("displaycolourdepth")] ?: 255f) shader.setUniformf("quant", shaderQuant[App.getConfigInt("displaycolourdepth")] ?: 255f)
shader.setUniformf("vibrancy", 1f, vo, vg, 1f)
shader.setUniformMatrix4fv("swizzler", swizzler, rng.nextInt(24), 16*4) shader.setUniformMatrix4fv("swizzler", swizzler, rng.nextInt(24), 16*4)
App.fullscreenQuad.render(shader, GL20.GL_TRIANGLES) App.fullscreenQuad.render(shader, GL20.GL_TRIANGLES)

View File

@@ -153,7 +153,7 @@ open class GameWorld(
var weatherbox = Weatherbox() var weatherbox = Weatherbox()
init { init {
weatherbox.initWith(WeatherMixer.weatherDict["generic01"]!!, 7200L) weatherbox.initWith(WeatherMixer.weatherDict["generic01"]!!, 3600L)
val currentWeather = weatherbox.currentWeather val currentWeather = weatherbox.currentWeather
// TEST FILL WITH RANDOM VALUES // TEST FILL WITH RANDOM VALUES
(0..6).map { WeatherMixer.takeUniformRand(0f..1f) }.let { (0..6).map { WeatherMixer.takeUniformRand(0f..1f) }.let {
@@ -165,12 +165,13 @@ open class GameWorld(
weatherbox.windDir.p3 = it[6] weatherbox.windDir.p3 = it[6]
} }
(0..6).map { WeatherMixer.takeUniformRand(-1f..1f) }.let { (0..6).map { WeatherMixer.takeUniformRand(-1f..1f) }.let {
weatherbox.windSpeed.pM2 = currentWeather.getRandomWindSpeed(it[0], it[1]) val pM3 = currentWeather.getRandomWindSpeed(it[1])
weatherbox.windSpeed.pM1 = currentWeather.getRandomWindSpeed(it[1], it[2]) weatherbox.windSpeed.pM2 = currentWeather.getRandomWindSpeed(pM3, it[1])
weatherbox.windSpeed.p0 = currentWeather.getRandomWindSpeed(it[2], it[3]) weatherbox.windSpeed.pM1 = currentWeather.getRandomWindSpeed(weatherbox.windSpeed.pM2, it[2])
weatherbox.windSpeed.p1 = currentWeather.getRandomWindSpeed(it[3], it[4]) weatherbox.windSpeed.p0 = currentWeather.getRandomWindSpeed(weatherbox.windSpeed.pM1, it[3])
weatherbox.windSpeed.p2 = currentWeather.getRandomWindSpeed(it[4], it[5]) weatherbox.windSpeed.p1 = currentWeather.getRandomWindSpeed(weatherbox.windSpeed.p0, it[4])
weatherbox.windSpeed.p3 = currentWeather.getRandomWindSpeed(it[5], it[6]) weatherbox.windSpeed.p2 = currentWeather.getRandomWindSpeed(weatherbox.windSpeed.p1, it[5])
weatherbox.windSpeed.p3 = currentWeather.getRandomWindSpeed(weatherbox.windSpeed.p2, it[6])
} }
// the savegame loader will overwrite whatever the initial value we have here // the savegame loader will overwrite whatever the initial value we have here

View File

@@ -177,6 +177,7 @@ class TitleScreen(batch: FlippingSpriteBatch) : IngameInstance(batch) {
printdbg(this, "Demo world not found, using empty world") printdbg(this, "Demo world not found, using empty world")
} }
this.world = demoWorld
// set initial time to summer // set initial time to summer
demoWorld.worldTime.addTime(WorldTime.DAY_LENGTH * 32) demoWorld.worldTime.addTime(WorldTime.DAY_LENGTH * 32)
@@ -213,7 +214,7 @@ class TitleScreen(batch: FlippingSpriteBatch) : IngameInstance(batch) {
IngameRenderer.setRenderedWorld(demoWorld) IngameRenderer.setRenderedWorld(demoWorld)
WeatherMixer.internalReset(this) WeatherMixer.internalReset(this)
WeatherMixer.titleScreenInitWeather(this.world.weatherbox) WeatherMixer.titleScreenInitWeather(demoWorld.weatherbox)
// load a half-gradient texture that would be used throughout the titlescreen and its sub UIs // load a half-gradient texture that would be used throughout the titlescreen and its sub UIs

View File

@@ -359,8 +359,10 @@ class BasicDebugInfoWindow : UICanvas() {
val schedYstart = App.scr.height - 140 - 120 - 13f * (weatherbox.weatherSchedule.size + 3) val schedYstart = App.scr.height - 140 - 120 - 13f * (weatherbox.weatherSchedule.size + 3)
App.fontSmallNumbers.draw(batch, "$ccY== WeatherSched [${weatherbox.weatherSchedule.size}] ==", drawXf, schedYstart) App.fontSmallNumbers.draw(batch, "$ccY== WeatherSched [${weatherbox.weatherSchedule.size}] ==", drawXf, schedYstart)
weatherbox.weatherSchedule.forEachIndexed { index, weather -> weatherbox.weatherSchedule.forEachIndexed { index, weather ->
val sek = if (index == 0) weatherbox.updateAkku else 0 val sek = if (index == 1) weatherbox.updateAkku else 0
App.fontSmallNumbers.draw(batch, "$ccY${weather.weather.identifier} $ccG${weather.duration - sek}", drawXf, schedYstart + 13 * (index + 1)) val cc1 = if (index == 0) ccK else ccY
val cc2 = if (index == 0) ccK else ccG
App.fontSmallNumbers.draw(batch, "$cc1${weather.weather.identifier} $cc2${weather.duration - sek}", drawXf, schedYstart + 13 * (index + 1))
} }
} }

View File

@@ -29,18 +29,26 @@ data class BaseModularWeather(
val cloudGamma: Vector2, val cloudGamma: Vector2,
val cloudGammaVariance: Vector2, val cloudGammaVariance: Vector2,
var clouds: List<CloudProps>, // sorted by CloudProps.probability var clouds: List<CloudProps>, // sorted by CloudProps.probability
val shaderVibrancy: FloatArray,
val mixFrom: String? = null, val mixFrom: String? = null,
val mixPercentage: Double? = null, val mixPercentage: Double? = null,
) { ) {
/** /**
* @param rnd random number between -1 and +1 * @param rnd random number between -1 and +1
*/ */
fun getRandomWindSpeed(old: Float, rnd: Float): Float { fun getRandomWindSpeed(old: Float, rnd: Float): Float {
val v = 1f + rnd.absoluteValue * windSpeedVariance val v = 1f + rnd.absoluteValue * windSpeedVariance
val r = if (rnd < 0) windSpeed / v else windSpeed * v val r = if (rnd < 0) windSpeed / v else windSpeed * v
return FastMath.interpolateLinear(windSpeedDamping, old, r) return FastMath.interpolateLinear(1f - windSpeedDamping, old, r)
}
fun getRandomWindSpeed(rnd: Float): Float {
val v = 1f + rnd.absoluteValue * windSpeedVariance
val r = if (rnd < 0) windSpeed / v else windSpeed * v
return r
} }
fun getRandomCloudGamma(rnd1: Float, rnd2: Float): Vector2 { fun getRandomCloudGamma(rnd1: Float, rnd2: Float): Vector2 {

View File

@@ -64,7 +64,8 @@ internal object WeatherMixer : RNGConsumer {
0f, 0f,
Vector2(1f, 1f), Vector2(1f, 1f),
Vector2(0f, 0f), Vector2(0f, 0f),
listOf() listOf(),
floatArrayOf(1f, 1f)
) )
override val RNG = HQRNG() override val RNG = HQRNG()
@@ -169,7 +170,7 @@ internal object WeatherMixer : RNGConsumer {
weatherDB[weather.classification]!!.add(weather) weatherDB[weather.classification]!!.add(weather)
} }
weatherDict["titlescreen"] = weatherDB[WEATHER_GENERIC]!![0].copy(windSpeed = 1f) weatherDict["titlescreen"] = weatherDB[WEATHER_GENERIC]!![0].copy(identifier = "titlescreen", windSpeed = 1f)
} }
/** /**
@@ -459,7 +460,8 @@ internal object WeatherMixer : RNGConsumer {
} }
private fun initClouds(currentWeather: BaseModularWeather) { private fun initClouds(currentWeather: BaseModularWeather) {
val hCloudSize = 1024f clouds.clear()
cloudsSpawned = 0
// 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 // it does converge at ~6, but having it as an initial state does not make it stay converged
repeat((currentWeather.cloudChance * 1.333f).ceilToInt()) { repeat((currentWeather.cloudChance * 1.333f).ceilToInt()) {
@@ -731,13 +733,6 @@ internal object WeatherMixer : RNGConsumer {
val skyboxInJson = JSON.getString("skyboxGradColourMap") val skyboxInJson = JSON.getString("skyboxGradColourMap")
val lightbox = JSON.getString("daylightClut") val lightbox = JSON.getString("daylightClut")
val skybox = GdxColorMap(ModMgr.getGdxFile(modname, "$pathToImage/${skyboxInJson}"))
val daylight = GdxColorMap(ModMgr.getGdxFile(modname, "$pathToImage/${lightbox}"))
val identifier = JSON.getString("identifier")
val classification = JSON.getString("classification")
val cloudsMap = ArrayList<CloudProps>() val cloudsMap = ArrayList<CloudProps>()
val clouds = JSON["clouds"] val clouds = JSON["clouds"]
clouds.forEachSiblings { name, json -> clouds.forEachSiblings { name, json ->
@@ -755,13 +750,12 @@ internal object WeatherMixer : RNGConsumer {
return BaseModularWeather( return BaseModularWeather(
identifier = identifier, identifier = JSON.getString("identifier"),
json = JSON, json = JSON,
skyboxGradColourMap = skybox, skyboxGradColourMap = GdxColorMap(ModMgr.getGdxFile(modname, "$pathToImage/${skyboxInJson}")),
daylightClut = daylight, daylightClut = GdxColorMap(ModMgr.getGdxFile(modname, "$pathToImage/${lightbox}")),
classification = classification, classification = JSON.getString("classification"),
cloudChance = JSON.getFloat("cloudChance"), cloudChance = JSON.getFloat("cloudChance"),
windSpeed = JSON.getFloat("windSpeed"), windSpeed = JSON.getFloat("windSpeed"),
windSpeedVariance = JSON.getFloat("windSpeedVariance"), windSpeedVariance = JSON.getFloat("windSpeedVariance"),
@@ -769,6 +763,7 @@ internal object WeatherMixer : RNGConsumer {
cloudGamma = JSON["cloudGamma"].asFloatArray().let { Vector2(it[0], it[1]) }, cloudGamma = JSON["cloudGamma"].asFloatArray().let { Vector2(it[0], it[1]) },
cloudGammaVariance = JSON["cloudGammaVariance"].asFloatArray().let { Vector2(it[0], it[1]) }, cloudGammaVariance = JSON["cloudGammaVariance"].asFloatArray().let { Vector2(it[0], it[1]) },
clouds = cloudsMap, clouds = cloudsMap,
shaderVibrancy = JSON["shaderVibrancy"].asFloatArray()
) )
} }

View File

@@ -5,12 +5,11 @@ import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.graphics.g2d.TextureRegion import com.badlogic.gdx.graphics.g2d.TextureRegion
import com.badlogic.gdx.math.Vector3 import com.badlogic.gdx.math.Vector3
import com.jme3.math.FastMath import com.jme3.math.FastMath
import com.jme3.math.FastMath.PI
import com.jme3.math.FastMath.sin
import net.torvald.terrarum.App import net.torvald.terrarum.App
import net.torvald.terrarum.gameworld.GameWorld import net.torvald.terrarum.gameworld.GameWorld
import kotlin.math.absoluteValue import kotlin.math.*
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.math.sign
/** /**
* Created by minjaesong on 2023-08-21. * Created by minjaesong on 2023-08-21.
@@ -47,7 +46,7 @@ class WeatherObjectCloud(
scl(world.worldTime.timeDelta.toFloat()) scl(world.worldTime.timeDelta.toFloat())
) )
eigenAlpha = if (posZ < 1f) posZ.pow(0.5f) else -((posZ - 1f) / ALPHA_ROLLOFF_Z) + 1f eigenAlpha = if (posZ < 1f) posZ.pow(0.5f) else cosh((posZ - CLOUD_STAGE_DEPTH) / (0.75636f * CLOUD_STAGE_DEPTH)) - 1f //-((posZ - 1f) / CLOUD_STAGE_DEPTH) + 1f
val alphaMult = if (life < NEWBORN_GROWTH_TIME) val alphaMult = if (life < NEWBORN_GROWTH_TIME)
life / NEWBORN_GROWTH_TIME life / NEWBORN_GROWTH_TIME
@@ -60,13 +59,13 @@ class WeatherObjectCloud(
val lrCoord = screenCoordBottomLRforDespawnCalculation val lrCoord = screenCoordBottomLRforDespawnCalculation
if (lrCoord.x > WeatherMixer.oobMarginR || lrCoord.z < WeatherMixer.oobMarginL || posZ !in 0.0001f..ALPHA_ROLLOFF_Z + 1f || alpha < 0f) { if (lrCoord.x > WeatherMixer.oobMarginR || lrCoord.z < WeatherMixer.oobMarginL || posZ !in 0.0001f..CLOUD_STAGE_DEPTH + 1f || alpha < 0f) {
flagToDespawn = true flagToDespawn = true
despawnCode = if (lrCoord.x > WeatherMixer.oobMarginR) "OUT_OF_SCREEN_RIGHT" despawnCode = if (lrCoord.x > WeatherMixer.oobMarginR) "OUT_OF_SCREEN_RIGHT"
else if (lrCoord.z < WeatherMixer.oobMarginL) "OUT_OF_SCREEN_LEFT" else if (lrCoord.z < WeatherMixer.oobMarginL) "OUT_OF_SCREEN_LEFT"
else if (posZ < 0.0001f) "OUT_OF_SCREEN_TOO_CLOSE" else if (posZ < 0.0001f) "OUT_OF_SCREEN_TOO_CLOSE"
else if (posZ > ALPHA_ROLLOFF_Z + 1f) "OUT_OF_SCREEN_TOO_FAR" else if (posZ > CLOUD_STAGE_DEPTH + 1f) "OUT_OF_SCREEN_TOO_FAR"
else if (life >= lifespan + OLD_AGE_DECAY) "OLD_AGE" else if (life >= lifespan + OLD_AGE_DECAY) "OLD_AGE"
else if (alpha < 0f) "ALPHA_BELOW_ZERO" else if (alpha < 0f) "ALPHA_BELOW_ZERO"
else "UNKNOWN" else "UNKNOWN"
@@ -180,6 +179,7 @@ class WeatherObjectCloud(
fun worldYtoWorldZforScreenYof0(y: Float) = 1f - (y / H) // rearrange screenCoord equations to derive this eq :p fun worldYtoWorldZforScreenYof0(y: Float) = 1f - (y / H) // rearrange screenCoord equations to derive this eq :p
const val ALPHA_ROLLOFF_Z = 64f const val ALPHA_ROLLOFF_Z = 64f
const val CLOUD_STAGE_DEPTH = 256f
const val OLD_AGE_DECAY = 5000f const val OLD_AGE_DECAY = 5000f
const val NEWBORN_GROWTH_TIME = 1000f const val NEWBORN_GROWTH_TIME = 1000f

View File

@@ -1,12 +1,11 @@
package net.torvald.terrarum.weather package net.torvald.terrarum.weather
import com.badlogic.gdx.math.Vector2
import com.jme3.math.FastMath import com.jme3.math.FastMath
import net.torvald.terrarum.App.printdbg
import net.torvald.terrarum.floorToInt import net.torvald.terrarum.floorToInt
import net.torvald.terrarum.gameworld.GameWorld import net.torvald.terrarum.gameworld.GameWorld
import net.torvald.terrarum.gameworld.fmod import net.torvald.terrarum.gameworld.fmod
import org.apache.commons.math3.analysis.UnivariateFunction
import org.apache.commons.math3.analysis.interpolation.NevilleInterpolator
import org.apache.commons.math3.analysis.interpolation.SplineInterpolator
import java.util.* import java.util.*
data class WeatherSchedule(val weather: BaseModularWeather = WeatherMixer.DEFAULT_WEATHER, val duration: Long = 3600) data class WeatherSchedule(val weather: BaseModularWeather = WeatherMixer.DEFAULT_WEATHER, val duration: Long = 3600)
@@ -37,25 +36,37 @@ class Weatherbox {
val windDir = WeatherDirBox() // 0 .. 1.0 val windDir = WeatherDirBox() // 0 .. 1.0
val windSpeed = WeatherStateBox() // 0 .. arbitrarily large number val windSpeed = WeatherStateBox() // 0 .. arbitrarily large number
val weatherSchedule: MutableList<WeatherSchedule> = mutableListOf<WeatherSchedule>() val weatherSchedule = ArrayList<WeatherSchedule>()
val currentWeather: BaseModularWeather val oldWeather: BaseModularWeather
get() = weatherSchedule[0].weather get() = weatherSchedule[0].weather
val currentWeatherDuration: Long val currentWeather: BaseModularWeather
get() = weatherSchedule[1].weather
// val nextWeather: BaseModularWeather
// get() = weatherSchedule[2].weather
val oldWeatherDuration: Long
get() = weatherSchedule[0].duration get() = weatherSchedule[0].duration
val currentWeatherDuration: Long
get() = weatherSchedule[1].duration
@Transient val currentVibrancy = Vector2(1f, 1f)
val weatherBlend: Float
get() = updateAkku.toFloat() / currentWeatherDuration
fun initWith(initWeather: BaseModularWeather, duration: Long) { fun initWith(initWeather: BaseModularWeather, duration: Long) {
weatherSchedule.clear() weatherSchedule.clear()
weatherSchedule.add(WeatherSchedule(initWeather, duration)) weatherSchedule.add(WeatherSchedule(initWeather, duration))
weatherSchedule.add(WeatherSchedule(initWeather, duration))
} }
var updateAkku = 0L; private set var updateAkku = 0L; private set
fun update(world: GameWorld) { fun update(world: GameWorld) {
updateShaderParams()
updateWind(world) updateWind(world)
if (updateAkku >= currentWeatherDuration) { if (updateAkku >= currentWeatherDuration) {
// TODO add more random weathers // TODO add more random weathers
if (weatherSchedule.size == 1) { while (weatherSchedule.size < 3) {
val newName = if (currentWeather.identifier == "generic01") "overcast01" else "generic01" val newName = if (currentWeather.identifier == "generic01") "overcast01" else "generic01"
val newDuration = 7200L val newDuration = 7200L
weatherSchedule.add(WeatherSchedule(WeatherMixer.weatherDict[newName]!!, newDuration)) weatherSchedule.add(WeatherSchedule(WeatherMixer.weatherDict[newName]!!, newDuration))
@@ -64,7 +75,10 @@ class Weatherbox {
} }
// subtract akku by old currentWeatherDuration // subtract akku by old currentWeatherDuration
updateAkku -= weatherSchedule.removeAt(0).duration // printdbg(this, "Dequeueing a weather")
weatherSchedule.removeAt(0)
updateAkku -= oldWeatherDuration
} }
updateAkku += world.worldTime.timeDelta updateAkku += world.worldTime.timeDelta
@@ -76,6 +90,15 @@ class Weatherbox {
} }
windDir.update( world.worldTime.timeDelta / WIND_DIR_TIME_UNIT) { RNG.nextFloat() * 4f } windDir.update( world.worldTime.timeDelta / WIND_DIR_TIME_UNIT) { RNG.nextFloat() * 4f }
} }
private fun updateShaderParams() {
val (co, cg) = oldWeather.shaderVibrancy
val (no, ng) = currentWeather.shaderVibrancy
currentVibrancy.set(
FastMath.interpolateLinear(weatherBlend * 2, co, no),
FastMath.interpolateLinear(weatherBlend * 2, cg, ng),
)
}
} }

View File

@@ -31,6 +31,9 @@ const mat4 swizzler = mat4(
); );
uniform float quant = 255.0; // 64 steps -> 63.0; 256 steps -> 255.0 uniform float quant = 255.0; // 64 steps -> 63.0; 256 steps -> 255.0
vec4 quantVec = vec4(quant);
vec4 invQuant = vec4(1.0 / quant);
out vec4 fragColor; out vec4 fragColor;
const vec2 boolean = vec2(0.0, 1.0); const vec2 boolean = vec2(0.0, 1.0);
@@ -54,12 +57,12 @@ const mat4 ycocg_to_rgb = mat4(
vec4 nearestColour(vec4 inColor) { vec4 nearestColour(vec4 inColor) {
return floor(vec4(quant) * inColor) * vec4(1.0 / quant); return floor(quantVec * inColor) * invQuant;
} }
vec4 getDitherredDot(vec4 inColor) { vec4 getDitherredDot(vec4 inColor) {
vec4 bayerThreshold = swizzler * vec4(matrixNormaliser + texture(u_pattern, (gl_FragCoord.xy + rnd) * patternsize)); vec4 bayerThreshold = swizzler * vec4(matrixNormaliser + texture(u_pattern, (gl_FragCoord.xy + rnd) * patternsize));
return nearestColour(bayerThreshold * vec4(1.0 / quant) + inColor); return nearestColour(fma(bayerThreshold, invQuant, inColor));
} }