From 6d71270ca4d31931552fefe5d24805d9278d56fe Mon Sep 17 00:00:00 2001 From: Aaron <51387595+AzureAaron@users.noreply.github.com> Date: Fri, 6 Dec 2024 22:54:59 -0500 Subject: OkLab Colour Interpolation The implementation has been written to take advantage of Java's hardware FMA intrinsic (available on all platforms) and the objects get optimized away by escape analysis. Using OkLab for colour interpolation makes the gradient much more balanced and even when sampling more colours from it. --- .../hysky/skyblocker/skyblock/CompactDamage.java | 6 +- .../skyblock/item/CustomArmorAnimatedDyes.java | 29 +----- .../java/de/hysky/skyblocker/utils/ColorUtils.java | 23 +++++ .../java/de/hysky/skyblocker/utils/OkLabColor.java | 104 +++++++++++++++++++++ 4 files changed, 135 insertions(+), 27 deletions(-) create mode 100644 src/main/java/de/hysky/skyblocker/utils/OkLabColor.java (limited to 'src/main/java') diff --git a/src/main/java/de/hysky/skyblocker/skyblock/CompactDamage.java b/src/main/java/de/hysky/skyblocker/skyblock/CompactDamage.java index 8285a823..96de8e2e 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/CompactDamage.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/CompactDamage.java @@ -1,7 +1,7 @@ package de.hysky.skyblocker.skyblock; import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.skyblock.item.CustomArmorAnimatedDyes; +import de.hysky.skyblocker.utils.OkLabColor; import net.minecraft.entity.decoration.ArmorStandEntity; import net.minecraft.text.MutableText; import net.minecraft.text.Text; @@ -57,10 +57,10 @@ public class CompactDamage { int length = prettifiedDmg.length(); for (int i = 0; i < length; i++) { prettierCustomName.append(Text.literal(prettifiedDmg.substring(i, i + 1)).withColor( - CustomArmorAnimatedDyes.interpolate( + OkLabColor.interpolate( SkyblockerConfigManager.get().uiAndVisuals.compactDamage.critDamageGradientStart.getRGB() & 0x00FFFFFF, SkyblockerConfigManager.get().uiAndVisuals.compactDamage.critDamageGradientEnd.getRGB() & 0x00FFFFFF, - i / (length - 1.0) + i / (length - 1.0f) ) )); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorAnimatedDyes.java b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorAnimatedDyes.java index 0621fd24..48f345c4 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorAnimatedDyes.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorAnimatedDyes.java @@ -9,6 +9,7 @@ import de.hysky.skyblocker.annotations.Init; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.OkLabColor; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.command.argumenttypes.color.ColorArgumentType; import dev.isxander.yacl3.config.v2.api.SerialEntry; @@ -21,7 +22,6 @@ import net.minecraft.command.CommandRegistryAccess; import net.minecraft.item.ItemStack; import net.minecraft.registry.tag.ItemTags; import net.minecraft.text.Text; -import net.minecraft.util.math.MathHelper; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; @@ -102,25 +102,6 @@ public class CustomArmorAnimatedDyes { return animatedDye.interpolate(trackedState); } - //Credit to https://codepen.io/OliverBalfour/post/programmatically-making-gradients - public static int interpolate(int firstColor, int secondColor, double percentage) { - int r1 = MathHelper.square((firstColor >> 16) & 0xFF); - int g1 = MathHelper.square((firstColor >> 8) & 0xFF); - int b1 = MathHelper.square(firstColor & 0xFF); - - int r2 = MathHelper.square((secondColor >> 16) & 0xFF); - int g2 = MathHelper.square((secondColor >> 8) & 0xFF); - int b2 = MathHelper.square(secondColor & 0xFF); - - double inverse = 1d - percentage; - - int r3 = (int) Math.floor(Math.sqrt(r1 * inverse + r2 * percentage)); - int g3 = (int) Math.floor(Math.sqrt(g1 * inverse + g2 * percentage)); - int b3 = (int) Math.floor(Math.sqrt(b1 * inverse + b2 * percentage)); - - return (r3 << 16) | (g3 << 8 ) | b3; - } - private static class AnimatedDyeStateTracker { private int sampleCounter; private boolean onBackCycle = false; @@ -150,12 +131,12 @@ public class CustomArmorAnimatedDyes { if (stateTracker.shouldCycleBack(samples, cycleBack)) stateTracker.onBackCycle = true; if (stateTracker.onBackCycle) { - double percent = (1d / (double) samples) * stateTracker.getAndDecrement(); + float percent = (1f / samples) * stateTracker.getAndDecrement(); //Go back to normal cycle once we've cycled all the way back if (stateTracker.sampleCounter == 0) stateTracker.onBackCycle = false; - int interpolatedColor = CustomArmorAnimatedDyes.interpolate(color1, color2, percent); + int interpolatedColor = OkLabColor.interpolate(color1, color2, percent); stateTracker.lastColor = interpolatedColor; return interpolatedColor; @@ -164,8 +145,8 @@ public class CustomArmorAnimatedDyes { //This will only happen if cycleBack is false if (stateTracker.sampleCounter == samples) stateTracker.sampleCounter = 0; - double percent = (1d / (double) samples) * stateTracker.getAndIncrement(); - int interpolatedColor = CustomArmorAnimatedDyes.interpolate(color1, color2, percent); + float percent = (1f / samples) * stateTracker.getAndIncrement(); + int interpolatedColor = OkLabColor.interpolate(color1, color2, percent); stateTracker.lastColor = interpolatedColor; diff --git a/src/main/java/de/hysky/skyblocker/utils/ColorUtils.java b/src/main/java/de/hysky/skyblocker/utils/ColorUtils.java index 2c8f5e4a..e132f7e5 100644 --- a/src/main/java/de/hysky/skyblocker/utils/ColorUtils.java +++ b/src/main/java/de/hysky/skyblocker/utils/ColorUtils.java @@ -1,6 +1,7 @@ package de.hysky.skyblocker.utils; import net.minecraft.util.DyeColor; +import net.minecraft.util.math.MathHelper; public class ColorUtils { /** @@ -23,4 +24,26 @@ public class ColorUtils { public static float[] getFloatComponents(DyeColor dye) { return getFloatComponents(dye.getEntityColor()); } + + /** + * Interpolates linearly between two colours. + */ + //Credit to https://codepen.io/OliverBalfour/post/programmatically-making-gradients + public static int interpolate(int firstColor, int secondColor, double percentage) { + int r1 = MathHelper.square((firstColor >> 16) & 0xFF); + int g1 = MathHelper.square((firstColor >> 8) & 0xFF); + int b1 = MathHelper.square(firstColor & 0xFF); + + int r2 = MathHelper.square((secondColor >> 16) & 0xFF); + int g2 = MathHelper.square((secondColor >> 8) & 0xFF); + int b2 = MathHelper.square(secondColor & 0xFF); + + double inverse = 1d - percentage; + + int r3 = (int) Math.floor(Math.sqrt(r1 * inverse + r2 * percentage)); + int g3 = (int) Math.floor(Math.sqrt(g1 * inverse + g2 * percentage)); + int b3 = (int) Math.floor(Math.sqrt(b1 * inverse + b2 * percentage)); + + return (r3 << 16) | (g3 << 8 ) | b3; + } } diff --git a/src/main/java/de/hysky/skyblocker/utils/OkLabColor.java b/src/main/java/de/hysky/skyblocker/utils/OkLabColor.java new file mode 100644 index 00000000..ce5c9f1a --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/OkLabColor.java @@ -0,0 +1,104 @@ +package de.hysky.skyblocker.utils; + +/** + * Implements color interpolation in the OkLab color space. + * + * @see OkLab Colour Space + * @see Gamma Correct Rendering + */ +public class OkLabColor { + + /** + * Converts a linear SRGB color to the OkLab color space. + * + * @param r the linearized red channel + * @param g the linearized green channel + * @param b the linearized blue channel + */ + private static Lab linearSRGB2OkLab(float r, float g, float b) { + float l = Math.fma(0.4122214708f, r, Math.fma(0.5363325363f, g, 0.0514459929f * b)); + float m = Math.fma(0.2119034982f, r, Math.fma(0.6806995451f, g, 0.1073969566f * b)); + float s = Math.fma(0.0883024619f, r, Math.fma(0.2817188376f, g, 0.6299787005f * b)); + + float l_ = (float) Math.cbrt(l); + float m_ = (float) Math.cbrt(m); + float s_ = (float) Math.cbrt(s); + + float L = Math.fma(0.2104542553f, l_, Math.fma(+0.7936177850f, m_, -0.0040720468f * s_)); + float A = Math.fma(1.9779984951f, l_, Math.fma(-2.4285922050f, m_, +0.4505937099f * s_)); + float B = Math.fma(0.0259040371f, l_, Math.fma(+0.7827717662f, m_, -0.8086757660f * s_)); + + return new Lab(L, A, B); + } + + /** + * Converts a color in the OkLab color space to linear SRGB. + */ + private static RGB okLab2LinearSRGB(float L, float A, float B) { + float l_ = L + 0.3963377774f * A + 0.2158037573f * B; + float m_ = L - 0.1055613458f * A - 0.0638541728f * B; + float s_ = L - 0.0894841775f * A - 1.2914855480f * B; + + float l = l_ * l_ * l_; + float m = m_ * m_ * m_; + float s = s_ * s_ * s_; + + float r = Math.fma(+4.0767416621f, l, Math.fma(-3.3077115913f, m, +0.2309699292f * s)); + float g = Math.fma(-1.2684380046f, l, Math.fma(+2.6097574011f, m, -0.3413193965f * s)); + float b = Math.fma(-0.0041960863f, l, Math.fma(-0.7034186147f, m, +1.7076147010f * s)); + + return new RGB(r, g, b); + } + + /** + * Converts {@code channel} from RGB to linear SRGB. + */ + private static float linearize(float channel) { + return channel <= 0.04045f ? channel / 12.92f : (float) Math.pow((channel + 0.055f) / 1.055f, 2.4f); + } + + /** + * Converts {@code channel} from linear SRGB to RGB. + */ + private static float delinearize(float channel) { + return channel <= 0.0031308f ? channel * 12.92f : Math.fma(1.055f, (float) Math.pow(channel, 1.0f / 2.4f), -0.055f); + } + + /** + * Interpolates two colors using the OkLab color space. + * + * @param firstColor the RGB color at the left end of the gradient + * @param secondColor the RGB color at the right end of the gradient + * @param progress a float from [0, 1] representing the position in the gradient + * + * @return the interpolated color in the RGB format + */ + //Escape analysis should hopefully take care of the objects :') + public static int interpolate(int firstColor, int secondColor, float progress) { + //Normalize colours to a range of [0, 1] + float normalizedR1 = ((firstColor >> 16) & 0xFF) / 255f; + float normalizedG1 = ((firstColor >> 8) & 0xFF) / 255f; + float normalizedB1 = (firstColor & 0xFF) / 255f; + + float normalizedR2 = ((secondColor >> 16) & 0xFF) / 255f; + float normalizedG2 = ((secondColor >> 8) & 0xFF) / 255f; + float normalizedB2 = (secondColor & 0xFF) / 255f; + + Lab lab1 = linearSRGB2OkLab(linearize(normalizedR1), linearize(normalizedG1), linearize(normalizedB1)); + Lab lab2 = linearSRGB2OkLab(linearize(normalizedR2), linearize(normalizedG2), linearize(normalizedB2)); + + float L = Math.fma(progress, (lab2.l - lab1.l), lab1.l); + float A = Math.fma(progress, (lab2.a - lab1.a), lab1.a); + float B = Math.fma(progress, (lab2.b - lab1.b), lab1.b); + + RGB rgb = okLab2LinearSRGB(L, A, B); + int r = Math.clamp((int) (delinearize(rgb.r) * 255f), 0, 255); + int g = Math.clamp((int) (delinearize(rgb.g) * 255f), 0, 255); + int b = Math.clamp((int) (delinearize(rgb.b) * 255f), 0, 255); + + return (r << 16) | (g << 8) | b; + } + + private record Lab(float l, float a, float b) {} + private record RGB(float r, float g, float b) {} +} -- cgit