The bug
This has been an issue for a while. It's especially noticeable at level 3 of the side of grass. See the screenshots, where I placed a row of grass on a row of dirt. With mipmaps, half of the grass block is practically black at the 'optimal' distance where the 2x2 mipmap or further is used.
How to reproduce
Open this world
[media]with these settings
[media] and look straight ahead without moving.
❌ Observe the dark patches with the side of the grass.
Proposed solution
Using Yarn mappings for 1.14.4, replace this method:
net.minecraft.client.texture.Sprite
private static int blendPixels(int int_1, int int_2, int int_3, int int_4) {
return SRGBAverager.average(int_1, int_2, int_3, int_4);
}
using these classes provided by @unknown:
[media][media][media]Original analysis
I've looked it up in MCP 9.30/9.31 (corresponding to Minecraft 1.10.2). In net.minecraft.client.renderer.texture.TextureUtil
, generateMipmapData
is responsible for mipmap generation. It turns out not to use proper weighting by the alpha channel. Anything that isn't fully transparent is given the full weight, and anything that is is treated as black. That's where the darkness comes from.
The correct model is most natural when using colors premultiplied by alpha: just average everything. If you don't use premultiplied colors, you must multiply colors by alpha before averaging and divide the resulting color by the resulting alpha afterward.
Premultiplied also has the advantage that any blending steps you don't control directly work correctly, like texture filtering and frame buffer blending. Minecraft won't really experience either the advantages or the disadvantages, but it's the cleaner model that would've prevented problems like this bug from the start. That's something to consider.
Adding to the problem is that Minecraft tries to apply gamma correction to the alpha channel. That blows up the opacity, only emphasizing exactly those texels that are too dark. Gamma correction of the alpha channel doesn't make sense anyway because colors can deviate either way depending on what's blended with what. The general standard—which is also what you'll get natively if you enable frame buffer and texture sRGB—is to use sRGB (which is a little more involved than x^2.2) for colors and keep the alpha channel linear.
Linked issues
is duplicated by 9
Attachments
Comments 45
@unknown: Yes, looks like it! Good catch. If I end up making a proof of concept mod, I'll include a burning mob in the screenshots which should demonstrate it nicely.
Here's a proof of concept based on MCP 9.37/Minecraft 1.11.2. Put the Java files in example/jonathan2520 (next to Start.java) as indicated by the package. In net.minecraft.client.renderer.texture.TextureUtil, change the line
aint2[i1 + j1 * j] = blendColors(aint1[k1 + 0], aint1[k1 + 1], aint1[k1 + 0 + l], aint1[k1 + 1 + l], flag);
to
aint2[i1 + j1 * j] = example.jonathan2520.SRGBAverager.average(aint1[k1 + 0], aint1[k1 + 1], aint1[k1 + 0 + l], aint1[k1 + 1 + l]);
Full changes:
Proper weighting by alpha channel, obviously.
Proper sRGB.
According to the new model, if all four inputs are entirely transparent, the color is undefined. (Kind of like the Invisible Pink Unicorn. No way to tell from its appearance that it's supposed to be pink.) I've defined it by using an unweighted average in that case. Minecraft originally made it black. Doesn't really matter, I think, but there it is.
Removed the special case where if at least one texel in the entire texture/atlas was fully transparent, alpha values under 96 would be cut to 0. Might serve some purpose but I don't see sense in it and it's inconvenient. It can be re-added if necessary.
Although I haven't benchmarked, it should be significantly faster. For starters, all 4 Math.pow calls are gone. I came up with a table-based sRGB encoding scheme that's simple, fast and exact.
Indeed, it fixes the black edges around fire as well.
Some more observations:
Minecraft uses interpolation between mipmaps (GL_NEAREST_MIPMAP_LINEAR). That's what I remembered but the hard transitions to darkness on the side of grass threw me for a loop. What even causes those? Anyway, that interpolation sans premultiplication can cause further artifacts. The hard pixel art edges in Minecraft's textures make that as bad as it can be. It's also a case where my preservation of invisible colors can reduce the ensuing artifacts, if source images are authored with sensible invisible colors which they generally aren't. I guess it may be better to process the mipmap pyramid in reverse afterward, using the color of the lower-res mipmap to replace invisible colors. That will catch the worst of it regardless of authoring. You could actually do it in the forward pass because you only need to propagate back one level.
I don't know the full pipeline that well. If I did I might've been able to experiment with some more stuff like premultiplication. (Very invasive, requiring adjustments to all blend functions and colors passed to OpenGL. Would also have to check for pure alpha testing without blending which doesn't play nicely with it.) Oh well. Not like it or the work I've done so far is going to accomplish much anyway until Mojang sees it, which could take a while.
The sRGB constant I called alpha is really called a. What's in a name?
I did just try to make Minecraft use premultiplication. Turns out OpenGL is abstracted away enough to get away with a few hooks to adjust colors and blend functions. For the most part things look okay. But there's one major problem: fixed-function fog can't deal with premultiplication. Transparent stuff in the distance (water, clouds, stained glass, …) turns bright white. The general solution is to do the right calculation per fragment, which requires a fragment shader. So probably not going to happen for now.
An observation with my change to mipmap generation (and without the premultiplication stuff from above) is that leaves keep some holes in the distance even with mipmaps enabled. Normally they become darker and entirely opaque with mipmaps, for the same reason the uglier problems with mipmaps occur. Opacification is a side effect of gamma correction of the alpha channel, pushing it over the alpha test reference more easily. I explicitly took that out because it's nonsense.
Although the transparency introduced by my patch would be expected for transparent leaves, I must admit I liked the shimmer-free look of opaque leaves. I've never been able to decide what's better. If you want to, you can reintroduce the opacifying behavior on top of my mipmap generation by sRGB-decoding the reference alpha passed to glAlphaFunc. Where 0.5 is currently passed to render stuff like leaves, you'd pass ((0.5+0.055)/(1+0.055))^2.4 ≈ 0.214. The result will be virtually identical, except all the other problems like darkening and blend opacification will be gone.
I think this will be the final comment for now. Definitely enough analysis. The minimally invasive patch that I posted solves most of the problem with minimal regression potential. I'm putting my money on that.
Its going to be like permanently stuck in the game, because Mojang doesnt see it or something, i mean its been in the game for 4 years
I found out how to fix this. Take every pixel of a broken texture that has completely opaque and transparent pixels, and blur all of the pixels together in an image editing program to get the average color. Use the color picker on that pixel, erase the pixel, then replace every transparent pixel with that averaged color, at 49% or less opacity. Pixels under 50% opacity are invisible (unless they're on blocks like water or stained glass), but will be blurred with the mipmapping. Here's the overlay texture on the side of a grass block after I fixed it:
[media]The reason you should use 49% opacity is because the lower the opacity, the lower the color quality will be. If you use 1% opacity, it'll probably get rounded to RGB(127,127,127). 49% is the highest opacity that will still be invisible but have the highest quality.
Sodium has this bug fixed since 0.5.0:
— Mip-mapping has been greatly improved so that terrain no longer has a black outline around certain textures when viewed at extreme angles. (Note: This has the consequence that some textures, such as those used for leaves, may seem slightly brighter than they did before. However, we think this generally looks more accurate to the original texture work, and believe the original behavior was a bug.)
[media][media]
(Don't pay attention to the touch buttons, I tested this fix on PojavLauncher)
Can confirm in the latest MC version, by the way. Mipmaps generation is still broken in vanilla Minecraft.
Do you think this is also causing the fire animation of a burning mob to get black / dark at the edges when you move away?