diff --git a/src/main/java/de/bixilon/minosoft/data/text/RGBColor.kt b/src/main/java/de/bixilon/minosoft/data/text/RGBColor.kt index f10357363..fb5f84d9c 100644 --- a/src/main/java/de/bixilon/minosoft/data/text/RGBColor.kt +++ b/src/main/java/de/bixilon/minosoft/data/text/RGBColor.kt @@ -27,6 +27,12 @@ class RGBColor(val rgba: Int) : ChatCode, TextFormattable { constructor(red: Double, green: Double, blue: Double, alpha: Double = 1.0) : this(red.toFloat(), green.toFloat(), blue.toFloat(), alpha.toFloat()) + val argb: Int + get() = (alpha shl 24) or (red shl 16) or (green shl 8) or blue + + val abgr: Int + get() = (alpha shl 24) or (blue shl 16) or (green shl 8) or red + val alpha: @IntRange(from = 0L, to = 255L) Int get() = rgba and 0xFF diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/system/base/texture/texture/AbstractTexture.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/system/base/texture/texture/AbstractTexture.kt index d78a08718..5a694e6e3 100644 --- a/src/main/java/de/bixilon/minosoft/gui/rendering/system/base/texture/texture/AbstractTexture.kt +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/system/base/texture/texture/AbstractTexture.kt @@ -15,11 +15,11 @@ package de.bixilon.minosoft.gui.rendering.system.base.texture.texture import de.bixilon.minosoft.data.assets.AssetsManager import de.bixilon.minosoft.data.registries.ResourceLocation -import de.bixilon.minosoft.data.text.RGBColor import de.bixilon.minosoft.gui.rendering.system.base.texture.TextureStates import de.bixilon.minosoft.gui.rendering.system.base.texture.TextureTransparencies import de.bixilon.minosoft.gui.rendering.system.opengl.texture.OpenGLTextureArray import de.bixilon.minosoft.gui.rendering.textures.properties.ImageProperties +import example.jonathan2520.SRGBAverager import glm_.vec2.Vec2 import glm_.vec2.Vec2i import org.lwjgl.BufferUtils @@ -41,119 +41,54 @@ interface AbstractTexture { fun load(assetsManager: AssetsManager) - fun generateMipMaps(): Array> { - val ret: MutableList> = mutableListOf() - var lastBuffer = data!! - var lastSize = size - for (i in 0 until OpenGLTextureArray.MAX_MIPMAP_LEVELS) { - val size = Vec2i(size.x shr i, size.y shr i) - if (i != 0 && size.x != 0 && size.y != 0) { - lastBuffer = generateMipmap(lastBuffer, lastSize, size) - lastSize = size + fun generateMipMaps(): Array { + val images: MutableList = mutableListOf() + + var data = data!! + + images += data + + for (i in 1 until OpenGLTextureArray.MAX_MIPMAP_LEVELS) { + val mipMapSize = Vec2i(size.x shr i, size.y shr i) + if (mipMapSize.x <= 0 || mipMapSize.y <= 0) { + break } - ret += Pair(size, lastBuffer) + data = generateMipmap(data, Vec2i(size.x shr (i - 1), size.y shr (i - 1))) + images += data } - return ret.toTypedArray() + return images.toTypedArray() } + private fun generateMipmap(origin: ByteBuffer, oldSize: Vec2i): ByteBuffer { + // No Vec2i: performance reasons + val oldSizeX = oldSize.x + val newSizeX = oldSizeX shr 1 - private fun ByteBuffer.getRGB(start: Int): RGBColor { - return RGBColor(get(start), get(start + 1), get(start + 2), get(start + 3)) - } - - private fun ByteBuffer.setRGB(start: Int, color: RGBColor) { - put(start, color.red.toByte()) - put(start + 1, color.green.toByte()) - put(start + 2, color.blue.toByte()) - put(start + 3, color.alpha.toByte()) - } - - @Deprecated(message = "This is garbage, will be improved soon...") - private fun generateMipmap(biggerBuffer: ByteBuffer, oldSize: Vec2i, newSize: Vec2i): ByteBuffer { - val sizeFactor = oldSize / newSize - val buffer = BufferUtils.createByteBuffer(biggerBuffer.capacity() shr 1) + val buffer = BufferUtils.createByteBuffer(origin.capacity() shr 1) buffer.limit(buffer.capacity()) - fun getRGB(x: Int, y: Int): RGBColor { - return biggerBuffer.getRGB((y * oldSize.x + x) * 4) + fun getRGB(x: Int, y: Int): Int { + return origin.getInt((y * oldSizeX + x) * 4) } - fun setRGB(x: Int, y: Int, color: RGBColor) { - buffer.setRGB((y * newSize.x + x) * 4, color) + fun setRGB(x: Int, y: Int, color: Int) { + buffer.putInt((y * newSizeX + x) * 4, color) } - for (y in 0 until newSize.y) { - for (x in 0 until newSize.x) { + for (y in 0 until (oldSize.y shr 1)) { + for (x in 0 until newSizeX) { + val xOffset = x * 2 + val yOffset = y * 2 - // check what is the most used transparency - val transparencyPixelCount = IntArray(TextureTransparencies.VALUES.size) - for (mixY in 0 until sizeFactor.y) { - for (mixX in 0 until sizeFactor.x) { - val color = getRGB(x * sizeFactor.x + mixX, y * sizeFactor.y + mixY) - when (color.alpha) { - 255 -> transparencyPixelCount[TextureTransparencies.OPAQUE.ordinal]++ - 0 -> transparencyPixelCount[TextureTransparencies.TRANSPARENT.ordinal]++ - else -> transparencyPixelCount[TextureTransparencies.TRANSLUCENT.ordinal]++ - } - } - } - var largest = 0 - for (count in transparencyPixelCount) { - if (count > largest) { - largest = count - } - } - var transparency: TextureTransparencies = TextureTransparencies.OPAQUE - for ((index, count) in transparencyPixelCount.withIndex()) { - if (count >= largest) { - transparency = TextureTransparencies[index] - break - } - } + val output = SRGBAverager.average( + getRGB(xOffset + 0, yOffset + 0), + getRGB(xOffset + 1, yOffset + 0), + getRGB(xOffset + 0, yOffset + 1), + getRGB(xOffset + 1, yOffset + 1), + ) - var count = 0 - var red = 0 - var green = 0 - var blue = 0 - var alpha = 0 - - // make magic for the most used transparency - for (mixY in 0 until sizeFactor.y) { - for (mixX in 0 until sizeFactor.x) { - val color = getRGB(x * sizeFactor.x + mixX, y * sizeFactor.y + mixY) - when (transparency) { - TextureTransparencies.OPAQUE -> { - if (color.alpha != 0xFF) { - continue - } - red += color.red - green += color.green - blue += color.blue - alpha += color.alpha - count++ - } - TextureTransparencies.TRANSPARENT -> { - } - TextureTransparencies.TRANSLUCENT -> { - red += color.red - green += color.green - blue += color.blue - alpha += color.alpha - count++ - } - } - } - } - - - - - - if (count == 0) { - count++ - } - setRGB(x, y, RGBColor(red / count, green / count, blue / count, alpha / count)) + setRGB(x, y, output) } } diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/system/opengl/texture/OpenGLTextureArray.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/system/opengl/texture/OpenGLTextureArray.kt index 043540a27..b52d757eb 100644 --- a/src/main/java/de/bixilon/minosoft/gui/rendering/system/opengl/texture/OpenGLTextureArray.kt +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/system/opengl/texture/OpenGLTextureArray.kt @@ -135,20 +135,21 @@ class OpenGLTextureArray( glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT) glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT) // glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST) - glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST) + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR) glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST) glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, MAX_MIPMAP_LEVELS - 1) - for (i in 0 until MAX_MIPMAP_LEVELS) { - glTexImage3D(GL_TEXTURE_2D_ARRAY, i, GL_RGBA, resolution shr i, resolution shr i, textures.size, 0, GL_RGBA, GL_UNSIGNED_BYTE, null as ByteBuffer?) + for (level in 0 until MAX_MIPMAP_LEVELS) { + glTexImage3D(GL_TEXTURE_2D_ARRAY, level, GL_RGBA, resolution shr level, resolution shr level, textures.size, 0, GL_RGBA, GL_UNSIGNED_BYTE, null as ByteBuffer?) } for (texture in textures) { val mipMaps = texture.generateMipMaps() val renderData = texture.renderData as OpenGLTextureData - for ((mipMapLevel, data) in mipMaps.withIndex()) { - glTexSubImage3D(GL_TEXTURE_2D_ARRAY, mipMapLevel, 0, 0, renderData.index, data.first.x, data.first.y, mipMapLevel + 1, GL_RGBA, GL_UNSIGNED_BYTE, data.second) + for ((level, data) in mipMaps.withIndex()) { + val size = texture.size shr level + glTexSubImage3D(GL_TEXTURE_2D_ARRAY, level, 0, 0, renderData.index, size.x, size.y, level + 1, GL_RGBA, GL_UNSIGNED_BYTE, data) } } diff --git a/src/main/java/example/jonathan2520/SRGBAverager.java b/src/main/java/example/jonathan2520/SRGBAverager.java new file mode 100644 index 000000000..89232ed9f --- /dev/null +++ b/src/main/java/example/jonathan2520/SRGBAverager.java @@ -0,0 +1,72 @@ +// Averaging of texels for mipmap generation. + +package example.jonathan2520; + +public class SRGBAverager { + private static final SRGBTable srgb = new SRGBTable(); + + public static int average(int c0, int c1, int c2, int c3) { + if ((((c0 | c1 | c2 | c3) ^ (c0 & c1 & c2 & c3)) & 0xff000000) == 0) { + // Alpha values are all equal. Simplifies computation somewhat. It's + // also a reasonable fallback when all alpha values are zero, in + // which case the resulting color would normally be undefined. + // Defining it like this allows code that uses invisible colors for + // whatever reason to work. Note that Minecraft's original code + // would set the color to black; this is added functionality. + + float r = srgb.decode(c0 & 0xff) + + srgb.decode(c1 & 0xff) + + srgb.decode(c2 & 0xff) + + srgb.decode(c3 & 0xff); + + float g = srgb.decode(c0 >> 8 & 0xff) + + srgb.decode(c1 >> 8 & 0xff) + + srgb.decode(c2 >> 8 & 0xff) + + srgb.decode(c3 >> 8 & 0xff); + + float b = srgb.decode(c0 >> 16 & 0xff) + + srgb.decode(c1 >> 16 & 0xff) + + srgb.decode(c2 >> 16 & 0xff) + + srgb.decode(c3 >> 16 & 0xff); + + return srgb.encode(0.25F * r) + | srgb.encode(0.25F * g) << 8 + | srgb.encode(0.25F * b) << 16 + | c0 & 0xff000000; + } else { + // The general case. Well-defined if at least one alpha value is + // not zero. If you do try to process all zeros, you get + // r = g = b = a = 0 which will NaN out in the division and produce + // invisible black. You could remove the other case if you're okay + // with that, but mind that producing or consuming a NaN causes an + // extremely slow exception handler to be run on many CPUs. + + float a0 = c0 >>> 24; + float a1 = c1 >>> 24; + float a2 = c2 >>> 24; + float a3 = c3 >>> 24; + + float r = a0 * srgb.decode(c0 & 0xff) + + a1 * srgb.decode(c1 & 0xff) + + a2 * srgb.decode(c2 & 0xff) + + a3 * srgb.decode(c3 & 0xff); + + float g = a0 * srgb.decode(c0 >> 8 & 0xff) + + a1 * srgb.decode(c1 >> 8 & 0xff) + + a2 * srgb.decode(c2 >> 8 & 0xff) + + a3 * srgb.decode(c3 >> 8 & 0xff); + + float b = a0 * srgb.decode(c0 >> 16 & 0xff) + + a1 * srgb.decode(c1 >> 16 & 0xff) + + a2 * srgb.decode(c2 >> 16 & 0xff) + + a3 * srgb.decode(c3 >> 16 & 0xff); + + float a = a0 + a1 + a2 + a3; + + return srgb.encode(r / a) + | srgb.encode(g / a) << 8 + | srgb.encode(b / a) << 16 + | (int) (0.25F * a + 0.5F) << 24; + } + } +} diff --git a/src/main/java/example/jonathan2520/SRGBCalculator.java b/src/main/java/example/jonathan2520/SRGBCalculator.java new file mode 100644 index 000000000..e01b80609 --- /dev/null +++ b/src/main/java/example/jonathan2520/SRGBCalculator.java @@ -0,0 +1,56 @@ +// Offers very precise sRGB encoding and decoding. + +// The actual values defining sRGB are alpha = 0.055 and gamma = 2.4. + +// This class works directly from that definition to take advantage of all +// available precision, unlike pre-rounded constants like 12.92 that cause a +// comparatively humongous discontinuity at a point that should be of +// differentiability class C^1. + +// Stored values are chosen to speed up bulk conversion somewhat. + +package example.jonathan2520; + +public class SRGBCalculator { + private final double decode_threshold; + private final double decode_slope; + private final double decode_multiplier; + private final double decode_addend; + private final double decode_exponent; + private final double encode_threshold; + private final double encode_slope; + private final double encode_multiplier; + private final double encode_addend; + private final double encode_exponent; + + public SRGBCalculator(double gamma, double alpha) { + encode_multiplier = alpha + 1.0; + decode_multiplier = 1.0 / encode_multiplier; + encode_addend = -alpha; + decode_addend = decode_multiplier * alpha; + encode_exponent = 1.0 / gamma; + decode_exponent = gamma; + decode_threshold = alpha / (gamma - 1.0); + encode_threshold = Math.pow(gamma * decode_threshold * decode_multiplier, gamma); + encode_slope = decode_threshold / encode_threshold; + decode_slope = encode_threshold / decode_threshold; + } + + public SRGBCalculator() { + this(2.4, 0.055); + } + + public double decode(double x) { + if (x < decode_threshold) + return decode_slope * x; + else + return Math.pow(x * decode_multiplier + decode_addend, decode_exponent); + } + + public double encode(double x) { + if (x < encode_threshold) + return encode_slope * x; + else + return Math.pow(x, encode_exponent) * encode_multiplier + encode_addend; + } +} diff --git a/src/main/java/example/jonathan2520/SRGBTable.java b/src/main/java/example/jonathan2520/SRGBTable.java new file mode 100644 index 000000000..0f441a443 --- /dev/null +++ b/src/main/java/example/jonathan2520/SRGBTable.java @@ -0,0 +1,71 @@ +// Offers fast sRGB encoding and decoding. + +// Decoding is a straightforward table look-up. + +// Encoding is a little more sophisticated. It's an exact conversion using about +// 4 kB of look-up tables that's also still quick. It relies on the fact that +// thresholds that would round to the next value have a minimum spacing of about +// 0.0003. That means that any range of 0.0003 contains at most one threshold. +// The to_int table contains the smaller value in the range. The threshold table +// contains the threshold above which the value should be one greater. + +// The minimum scale value that maintains proper spacing is 255 * encode_slope +// or about 3295.4. You can get away with a little bit less like 3200, taking +// advantage of the alignment of thresholds, but it's not really worth it. + +package example.jonathan2520; + +public class SRGBTable { + private final float scale; + private final float[] to_float; + private final float[] threshold; + private final byte[] to_int; + + public SRGBTable() { + this(new SRGBCalculator(), 3295.5F); + } + + public SRGBTable(SRGBCalculator calc, float scale) { + this.scale = scale; + to_float = new float[256]; + threshold = new float[256]; + to_int = new byte[(int) scale + 1]; + for (int i = 0; i < 255; ++i) { + to_float[i] = (float) calc.decode(i / 255.0); + double dthresh = calc.decode((i + 0.5) / 255.0); + float fthresh = (float) dthresh; + if (fthresh >= dthresh) + fthresh = Math.nextAfter(fthresh, -1); + threshold[i] = fthresh; + } + to_float[255] = 1; + threshold[255] = Float.POSITIVE_INFINITY; + int offset = 0; + for (int i = 0; i < 255; ++i) { + int up_to = (int) (threshold[i] * scale); + build_to_int_table(offset, up_to, (byte) i); + offset = up_to + 1; + } + build_to_int_table(offset, (int) scale, (byte) 255); + } + + private void build_to_int_table(int offset, int up_to, byte value) { + if (offset > up_to) + throw new IllegalArgumentException("scale is too small"); + while (offset <= up_to) + to_int[offset++] = value; + } + + // x in [0, 255] + public float decode(int x) { + return to_float[x]; + } + + // x in about [-0.0003, 1.00015]: tolerates rounding error on top of [0, 1] + public int encode(float x) { + int index = to_int[(int) (x * scale)] & 0xff; + if (x > threshold[index]) + ++index; + return index; + } +}