From d8966e81ed4ceac51c00d753169df55171bf9dac Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 25 Aug 2024 03:32:59 +0200 Subject: [PATCH] Downgrade quad materials by scanning their textures (#2666) --- .../chunk/compile/ChunkBuildBuffers.java | 4 + .../chunk/compile/pipeline/BlockRenderer.java | 99 +++++++++++++++++-- .../pipeline/SpriteContentsExtension.java | 7 ++ .../builder/ChunkMeshBufferBuilder.java | 6 +- .../vertex/format/ChunkVertexEncoder.java | 4 +- .../format/impl/CompactChunkVertex.java | 4 +- .../textures/mipmaps/SpriteContentsMixin.java | 27 ++--- .../textures/scan/SpriteContentsMixin.java | 74 ++++++++++++++ common/src/main/resources/sodium.mixins.json | 1 + 9 files changed, 190 insertions(+), 36 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/SpriteContentsExtension.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/scan/SpriteContentsMixin.java diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildBuffers.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildBuffers.java index d506ba3237..eae5389deb 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildBuffers.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildBuffers.java @@ -51,6 +51,10 @@ public ChunkModelBuilder get(Material material) { return this.builders.get(material.pass); } + public ChunkModelBuilder get(TerrainRenderPass pass) { + return this.builders.get(pass); + } + /** * Creates immutable baked chunk meshes from all non-empty scratch buffers. This is used after all blocks * have been rendered to pass the finished meshes over to the graphics card. This function can be called multiple diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/BlockRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/BlockRenderer.java index ab63a602e3..fee294d176 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/BlockRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/BlockRenderer.java @@ -1,5 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline; +import net.caffeinemc.mods.sodium.api.util.ColorABGR; import net.caffeinemc.mods.sodium.api.util.ColorARGB; import net.caffeinemc.mods.sodium.client.model.color.ColorProvider; import net.caffeinemc.mods.sodium.client.model.color.ColorProviderRegistry; @@ -9,8 +10,12 @@ import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadOrientation; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildBuffers; import net.caffeinemc.mods.sodium.client.render.chunk.compile.buffers.ChunkModelBuilder; +import net.caffeinemc.mods.sodium.client.render.chunk.terrain.DefaultTerrainRenderPasses; +import net.caffeinemc.mods.sodium.client.render.chunk.terrain.TerrainRenderPass; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.material.DefaultMaterials; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.material.Material; +import net.caffeinemc.mods.sodium.client.render.chunk.terrain.material.parameters.AlphaCutoffParameter; +import net.caffeinemc.mods.sodium.client.render.chunk.terrain.material.parameters.MaterialParameters; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TranslucentGeometryCollector; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.builder.ChunkMeshBufferBuilder; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexEncoder; @@ -28,6 +33,7 @@ import net.fabricmc.fabric.api.util.TriState; import net.minecraft.client.renderer.ItemBlockRenderTypes; import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.client.resources.model.BakedModel; import net.minecraft.core.BlockPos; import net.minecraft.world.level.block.state.BlockState; @@ -128,11 +134,9 @@ protected void processQuad(MutableQuadViewImpl quad) { material = DefaultMaterials.forRenderLayer(blendMode.blockRenderLayer == null ? type : blendMode.blockRenderLayer); } - ChunkModelBuilder builder = this.buffers.get(material); - this.colorizeQuad(quad, colorIndex); this.shadeQuad(quad, lightMode, emissive, shadeMode); - this.bufferQuad(quad, this.quadLightData.br, material, builder); + this.bufferQuad(quad, this.quadLightData.br, material); } private void colorizeQuad(MutableQuadViewImpl quad, int colorIndex) { @@ -150,12 +154,15 @@ private void colorizeQuad(MutableQuadViewImpl quad, int colorIndex) { } } - private void bufferQuad(MutableQuadViewImpl quad, float[] brightnesses, Material material, ChunkModelBuilder modelBuilder) { + private void bufferQuad(MutableQuadViewImpl quad, float[] brightnesses, Material material) { // TODO: Find a way to reimplement quad reorientation ModelQuadOrientation orientation = ModelQuadOrientation.NORMAL; ChunkVertexEncoder.Vertex[] vertices = this.vertices; Vector3f offset = this.posOffset; + float uSum = 0.0f; + float vSum = 0.0f; + for (int dstIndex = 0; dstIndex < 4; dstIndex++) { int srcIndex = orientation.getVertexIndex(dstIndex); @@ -167,21 +174,93 @@ private void bufferQuad(MutableQuadViewImpl quad, float[] brightnesses, Material // FRAPI uses ARGB color format; convert to ABGR. out.color = ColorARGB.toABGR(quad.color(srcIndex)); out.ao = brightnesses[srcIndex]; - out.u = quad.u(srcIndex); - out.v = quad.v(srcIndex); + + uSum += out.u = quad.u(srcIndex); + vSum += out.v = quad.v(srcIndex); out.light = quad.lightmap(srcIndex); } + var atlasSprite = SpriteFinderCache.forBlockAtlas().find(uSum / 4.0f, vSum / 4.0f); + var materialBits = material.bits(); ModelQuadFacing normalFace = quad.normalFace(); - if (material.isTranslucent() && this.collector != null) { + // attempt render pass downgrade if possible + var pass = material.pass; + var downgradedPass = attemptPassDowngrade(quad, atlasSprite, pass); + if (downgradedPass != null) { + pass = downgradedPass; + } + + // collect all translucent quads into the translucency sorting system if enabled + if (pass.isTranslucent() && this.collector != null) { this.collector.appendQuad(quad.getFaceNormal(), vertices, normalFace); } - ChunkMeshBufferBuilder vertexBuffer = modelBuilder.getVertexBuffer(normalFace); - vertexBuffer.push(vertices, material); + // if there was a downgrade from translucent to cutout, the material bits' alpha cutoff needs to be updated + if (downgradedPass != null && material == DefaultMaterials.TRANSLUCENT && pass == DefaultTerrainRenderPasses.CUTOUT) { + // ONE_TENTH and HALF are functionally the same so it doesn't matter which one we take here + materialBits = MaterialParameters.pack(AlphaCutoffParameter.ONE_TENTH, material.mipped); + } + + ChunkModelBuilder builder = this.buffers.get(pass); + ChunkMeshBufferBuilder vertexBuffer = builder.getVertexBuffer(normalFace); + vertexBuffer.push(vertices, materialBits); + + builder.addSprite(atlasSprite); + } + + private boolean validateQuadUVs(TextureAtlasSprite atlasSprite) { + // sanity check that the quad's UVs are within the sprite's bounds + var spriteUMin = atlasSprite.getU0(); + var spriteUMax = atlasSprite.getU1(); + var spriteVMin = atlasSprite.getV0(); + var spriteVMax = atlasSprite.getV1(); + + for (int i = 0; i < 4; i++) { + var u = this.vertices[i].u; + var v = this.vertices[i].v; + if (u < spriteUMin || u > spriteUMax || v < spriteVMin || v > spriteVMax) { + return false; + } + } + + return true; + } + + private TerrainRenderPass attemptPassDowngrade(MutableQuadViewImpl quad, TextureAtlasSprite sprite, TerrainRenderPass pass) { + boolean attemptDowngrade = true; + boolean hasNonOpaqueVertex = false; + + for (int i = 0; i < 4; i++) { + hasNonOpaqueVertex |= ColorABGR.unpackAlpha(this.vertices[i].color) != 0xFF; + } + + // don't do downgrade if some vertex is not fully opaque + if (pass.isTranslucent() && hasNonOpaqueVertex) { + attemptDowngrade = false; + } + + if (attemptDowngrade) { + attemptDowngrade = validateQuadUVs(sprite); + } + + if (attemptDowngrade) { + return getDowngradedPass(sprite, pass); + } + + return null; + } - modelBuilder.addSprite(SpriteFinderCache.forBlockAtlas().find(quad.getTexU(0), quad.getTexV(0))); + private static TerrainRenderPass getDowngradedPass(TextureAtlasSprite sprite, TerrainRenderPass pass) { + if (sprite.contents() instanceof SpriteContentsExtension contents) { + if (pass == DefaultTerrainRenderPasses.TRANSLUCENT && !contents.sodium$hasTranslucentPixels()) { + pass = DefaultTerrainRenderPasses.CUTOUT; + } + if (pass == DefaultTerrainRenderPasses.CUTOUT && !contents.sodium$hasTransparentPixels()) { + pass = DefaultTerrainRenderPasses.SOLID; + } + } + return pass; } } \ No newline at end of file diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/SpriteContentsExtension.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/SpriteContentsExtension.java new file mode 100644 index 0000000000..e54f894587 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/pipeline/SpriteContentsExtension.java @@ -0,0 +1,7 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline; + +public interface SpriteContentsExtension { + boolean sodium$hasTransparentPixels(); + + boolean sodium$hasTranslucentPixels(); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/builder/ChunkMeshBufferBuilder.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/builder/ChunkMeshBufferBuilder.java index d6155d27ba..574701eda2 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/builder/ChunkMeshBufferBuilder.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/builder/ChunkMeshBufferBuilder.java @@ -28,6 +28,10 @@ public ChunkMeshBufferBuilder(ChunkVertexType vertexType, int initialCapacity) { } public void push(ChunkVertexEncoder.Vertex[] vertices, Material material) { + this.push(vertices, material.bits()); + } + + public void push(ChunkVertexEncoder.Vertex[] vertices, int materialBits) { var vertexCount = vertices.length; if (this.count + vertexCount >= this.capacity) { @@ -35,7 +39,7 @@ public void push(ChunkVertexEncoder.Vertex[] vertices, Material material) { } this.encoder.write(MemoryUtil.memAddress(this.buffer, this.count * this.stride), - material, vertices, this.sectionIndex); + materialBits, vertices, this.sectionIndex); this.count += vertexCount; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/format/ChunkVertexEncoder.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/format/ChunkVertexEncoder.java index c0bfb4cff3..cac465580c 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/format/ChunkVertexEncoder.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/format/ChunkVertexEncoder.java @@ -1,9 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.vertex.format; -import net.caffeinemc.mods.sodium.client.render.chunk.terrain.material.Material; - public interface ChunkVertexEncoder { - long write(long ptr, Material material, Vertex[] vertices, int sectionIndex); + long write(long ptr, int materialBits, Vertex[] vertices, int sectionIndex); class Vertex { public float x; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/format/impl/CompactChunkVertex.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/format/impl/CompactChunkVertex.java index 970a6afb31..627391c3e1 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/format/impl/CompactChunkVertex.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/vertex/format/impl/CompactChunkVertex.java @@ -32,7 +32,7 @@ public GlVertexFormat getVertexFormat() { @Override public ChunkVertexEncoder getEncoder() { - return (ptr, material, vertices, section) -> { + return (ptr, materialBits, vertices, section) -> { // Calculate the center point of the texture region which is mapped to the quad float texCentroidU = 0.0f; float texCentroidV = 0.0f; @@ -61,7 +61,7 @@ public ChunkVertexEncoder getEncoder() { MemoryUtil.memPutInt(ptr + 4L, packPositionLo(x, y, z)); MemoryUtil.memPutInt(ptr + 8L, ColorHelper.multiplyRGB(vertex.color, vertex.ao)); MemoryUtil.memPutInt(ptr + 12L, packTexture(u, v)); - MemoryUtil.memPutInt(ptr + 16L, packLightAndData(light, material.bits(), section)); + MemoryUtil.memPutInt(ptr + 16L, packLightAndData(light, materialBits, section)); ptr += STRIDE; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/mipmaps/SpriteContentsMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/mipmaps/SpriteContentsMixin.java index 48a634487e..f965f8547a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/mipmaps/SpriteContentsMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/mipmaps/SpriteContentsMixin.java @@ -7,21 +7,17 @@ */ package net.caffeinemc.mods.sodium.mixin.features.textures.mipmaps; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.mojang.blaze3d.platform.NativeImage; import net.caffeinemc.mods.sodium.client.util.NativeImageHelper; import net.caffeinemc.mods.sodium.client.util.color.ColorSRGB; import net.minecraft.client.renderer.texture.SpriteContents; -import net.minecraft.resources.ResourceLocation; import net.minecraft.util.FastColor; import org.lwjgl.system.MemoryUtil; import org.objectweb.asm.Opcodes; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Mutable; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.*; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; @Mixin(SpriteContents.class) public class SpriteContentsMixin { @@ -30,20 +26,11 @@ public class SpriteContentsMixin { @Final private NativeImage originalImage; - // While Fabric allows us to @Inject into the constructor here, that's just a specific detail of FabricMC's mixin - // fork. Upstream Mixin doesn't allow arbitrary @Inject usage in constructor. However, we can use @ModifyVariable - // just fine, in a way that hopefully doesn't conflict with other mods. - // - // By doing this, we can work with upstream Mixin as well, as is used on Forge. While we don't officially - // support Forge, since this works well on Fabric too, it's fine to ensure that the diff between Fabric and Forge - // can remain minimal. Being less dependent on specific details of Fabric is good, since it means we can be more - // cross-platform. - @Redirect(method = "", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/texture/SpriteContents;originalImage:Lcom/mojang/blaze3d/platform/NativeImage;", opcode = Opcodes.PUTFIELD)) - private void sodium$beforeGenerateMipLevels(SpriteContents instance, NativeImage nativeImage, ResourceLocation identifier) { - // We're injecting after the "info" field has been set, so this is safe even though we're in a constructor. + @WrapOperation(method = "", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/texture/SpriteContents;originalImage:Lcom/mojang/blaze3d/platform/NativeImage;", opcode = Opcodes.PUTFIELD)) + private void sodium$beforeGenerateMipLevels(SpriteContents instance, NativeImage nativeImage, Operation original) { sodium$fillInTransparentPixelColors(nativeImage); - this.originalImage = nativeImage; + original.call(instance, nativeImage); } /** @@ -68,7 +55,7 @@ public class SpriteContentsMixin { float totalWeight = 0.0f; for (int pixelIndex = 0; pixelIndex < pixelCount; pixelIndex++) { - long pPixel = ppPixel + (pixelIndex * 4); + long pPixel = ppPixel + (pixelIndex * 4L); int color = MemoryUtil.memGetInt(pPixel); int alpha = FastColor.ABGR32.alpha(color); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/scan/SpriteContentsMixin.java b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/scan/SpriteContentsMixin.java new file mode 100644 index 0000000000..db106e65e4 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/mixin/features/textures/scan/SpriteContentsMixin.java @@ -0,0 +1,74 @@ +package net.caffeinemc.mods.sodium.mixin.features.textures.scan; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.mojang.blaze3d.platform.NativeImage; +import net.caffeinemc.mods.sodium.api.util.ColorABGR; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.SpriteContentsExtension; +import net.caffeinemc.mods.sodium.client.util.NativeImageHelper; +import net.minecraft.client.renderer.texture.SpriteContents; +import org.lwjgl.system.MemoryUtil; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.*; +import org.spongepowered.asm.mixin.injection.At; + +/** + * This mixin scans a {@link SpriteContents} for transparent and translucent pixels. This information is later used during mesh generation to reassign the render pass to either cutout if the sprite has no translucent pixels or solid if it doesn't even have any transparent pixels. + * + * @author douira + */ +@Mixin(SpriteContents.class) +public class SpriteContentsMixin implements SpriteContentsExtension { + @Mutable + @Shadow + @Final + private NativeImage originalImage; + + @Unique + public boolean sodium$hasTransparentPixels = false; + + @Unique + public boolean sodium$hasTranslucentPixels = false; + + /* + * Uses a WrapOperation here since Inject doesn't work on 1.20.1 forge. + */ + @WrapOperation(method = "", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/texture/SpriteContents;originalImage:Lcom/mojang/blaze3d/platform/NativeImage;", opcode = Opcodes.PUTFIELD)) + private void sodium$beforeGenerateMipLevels(SpriteContents instance, NativeImage nativeImage, Operation original) { + scanSpriteContents(nativeImage); + + original.call(instance, nativeImage); + } + + @Unique + private void scanSpriteContents(NativeImage nativeImage) { + final long ppPixel = NativeImageHelper.getPointerRGBA(nativeImage); + final int pixelCount = nativeImage.getHeight() * nativeImage.getWidth(); + + for (int pixelIndex = 0; pixelIndex < pixelCount; pixelIndex++) { + int color = MemoryUtil.memGetInt(ppPixel + (pixelIndex * 4L)); + int alpha = ColorABGR.unpackAlpha(color); + + // 25 is used as the threshold since the alpha cutoff is 0.1 + if (alpha <= 25) { // 0.1 * 255 + this.sodium$hasTransparentPixels = true; + } else if (alpha < 255) { + this.sodium$hasTranslucentPixels = true; + } + } + + // the image contains transparency also if there are translucent pixels, + // since translucent pixels prevent a downgrade to the opaque render pass just as transparent pixels do + this.sodium$hasTransparentPixels |= this.sodium$hasTranslucentPixels; + } + + @Override + public boolean sodium$hasTransparentPixels() { + return this.sodium$hasTransparentPixels; + } + + @Override + public boolean sodium$hasTranslucentPixels() { + return this.sodium$hasTranslucentPixels; + } +} diff --git a/common/src/main/resources/sodium.mixins.json b/common/src/main/resources/sodium.mixins.json index e110e980ee..9474414e02 100644 --- a/common/src/main/resources/sodium.mixins.json +++ b/common/src/main/resources/sodium.mixins.json @@ -83,6 +83,7 @@ "features.textures.animations.upload.SpriteContentsInterpolationMixin", "features.textures.mipmaps.MipmapGeneratorMixin", "features.textures.mipmaps.SpriteContentsMixin", + "features.textures.scan.SpriteContentsMixin", "features.world.biome.BiomeMixin", "workarounds.context_creation.WindowMixin", "workarounds.event_loop.RenderSystemMixin"