diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8103d3649..88a4fa05f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ asmCompile = "5.0.3" launchwrapper = "1.12" # End Frozen -lwjgl3 = "3.4.2-SNAPSHOT" +lwjgl3 = "3.4.2" # Must equal the LWJGL release shipped by lwjgl3ify. Do not bump without bumping lwjgl3ify. lwjgl3Release = "3.4.2" lwjgl3ify = "3.0.31" diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/AngelicaChunkBuildContext.java b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/AngelicaChunkBuildContext.java index ff94bbbd4..3f6ef6305 100644 --- a/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/AngelicaChunkBuildContext.java +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/AngelicaChunkBuildContext.java @@ -36,7 +36,6 @@ import org.embeddedt.embeddium.impl.render.chunk.RenderPassConfiguration; import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildBuffers; import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildContext; import org.embeddedt.embeddium.impl.render.chunk.data.MinecraftBuiltRenderSectionData; -import org.embeddedt.embeddium.impl.render.chunk.sprite.SpriteTransparencyLevel; import org.embeddedt.embeddium.impl.render.chunk.terrain.material.Material; import org.embeddedt.embeddium.impl.render.chunk.vertex.format.ChunkVertexEncoder; import org.embeddedt.embeddium.impl.util.QuadUtil; @@ -62,6 +61,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext { @Getter private final FloatArrayList teBoundsScratch = new FloatArrayList(); @Getter private final long[] deferredMask = new long[64]; private final VertexArrayQuadView quadView; + private final QuadMaterialSelector materialSelector = new QuadMaterialSelector(); private final boolean hasColoredLight; private final float[] tintResult = new float[3]; private List chunkLightSources; @@ -116,27 +116,6 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext { } } - private Material selectMaterial(Material material, TextureAtlasSprite sprite, boolean isShaderPackOverride, - boolean useRenderPassOptimization) { - // Don't apply transparency-based optimization when shader pack explicitly overrides the material - if (isShaderPackOverride) { - return material; - } - - if (useRenderPassOptimization - && sprite != null && sprite.getClass() == TextureAtlasSprite.class && !sprite.hasAnimationMetadata()) { - final var transparencyLevel = ((SpriteExtension) sprite).celeritas$getTransparencyLevel(); - if (transparencyLevel == SpriteTransparencyLevel.OPAQUE - && material == AngelicaRenderPassConfiguration.CUTOUT_MIPPED_MATERIAL) { - return AngelicaRenderPassConfiguration.SOLID_MATERIAL; - } else if (material == AngelicaRenderPassConfiguration.TRANSLUCENT_MATERIAL && transparencyLevel != SpriteTransparencyLevel.TRANSLUCENT) { - return AngelicaRenderPassConfiguration.CUTOUT_MIPPED_MATERIAL; - } - } - - return material; - } - @SuppressWarnings("unchecked") public void copyRawBuffer(int[] rawBuffer, int vertexCount, int[] vertexStates, int[] shaderOverrideBlockIds, @@ -174,9 +153,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext { final short originalBlockId = blockRenderContext.blockId; int stateIdx = 0; - boolean hasCachedMaterial = false; - TextureAtlasSprite lastSprite = null; - Material lastMaterial = null; + materialSelector.reset(material, isShaderPackOverride, useRenderPassOptimization); this.fluidQuadLightFace = null; for (int quadIdx = 0; quadIdx < numQuads; quadIdx++) { @@ -266,17 +243,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext { // Apply RGB block light tint from provider applyBlockLightTint(worldX, worldY, worldZ, vertices); - final Material correctMaterial; - if (material == AngelicaRenderPassConfiguration.TRANSLUCENT_MATERIAL && hasVertexTransparency) { - correctMaterial = material; - } else if (hasCachedMaterial && sprite == lastSprite) { - correctMaterial = lastMaterial; - } else { - correctMaterial = selectMaterial(material, sprite, isShaderPackOverride, useRenderPassOptimization); - lastSprite = sprite; - lastMaterial = correctMaterial; - hasCachedMaterial = true; - } + final Material correctMaterial = materialSelector.select(facing, sprite, hasVertexTransparency); final var builder = buffers.get(correctMaterial); final int shaderOverrideBlockId = shaderOverrideBlockIds[quadIdx * 4]; diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/QuadMaterialSelector.java b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/QuadMaterialSelector.java new file mode 100644 index 000000000..98ef5db65 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/QuadMaterialSelector.java @@ -0,0 +1,57 @@ +package com.gtnewhorizons.angelica.rendering.celeritas; + +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import org.embeddedt.embeddium.impl.model.quad.properties.ModelQuadFacing; +import org.embeddedt.embeddium.impl.render.chunk.sprite.SpriteTransparencyLevel; +import org.embeddedt.embeddium.impl.render.chunk.terrain.material.Material; + +class QuadMaterialSelector { + private Material material; + private boolean canDemote; + private int facesAtBaseMaterial; + private TextureAtlasSprite lastSprite; + private Material lastMaterial; + + void reset(Material material, boolean isShaderPackOverride, boolean useRenderPassOptimization) { + this.material = material; + // Don't apply transparency-based optimization when shader pack explicitly overrides the material + this.canDemote = useRenderPassOptimization && !isShaderPackOverride; + facesAtBaseMaterial = 0; + lastMaterial = null; + } + + Material select(ModelQuadFacing facing, TextureAtlasSprite sprite, boolean hasVertexTransparency) { + final int faceBit = 1 << facing.ordinal(); + final Material correctMaterial; + if ((facesAtBaseMaterial & faceBit) != 0 || (material == AngelicaRenderPassConfiguration.TRANSLUCENT_MATERIAL && hasVertexTransparency)) { + correctMaterial = material; + } else if (lastMaterial != null && sprite == lastSprite) { + correctMaterial = lastMaterial; + } else { + correctMaterial = demote(sprite); + lastSprite = sprite; + lastMaterial = correctMaterial; + } + if (correctMaterial == material) { + facesAtBaseMaterial |= faceBit; + } + return correctMaterial; + } + + private Material demote(TextureAtlasSprite sprite) { + if (canDemote && sprite != null && sprite.getClass() == TextureAtlasSprite.class && !sprite.hasAnimationMetadata()) { + final var transparencyLevel = transparencyLevel(sprite); + if (transparencyLevel == SpriteTransparencyLevel.OPAQUE && material == AngelicaRenderPassConfiguration.CUTOUT_MIPPED_MATERIAL) { + return AngelicaRenderPassConfiguration.SOLID_MATERIAL; + } else if (material == AngelicaRenderPassConfiguration.TRANSLUCENT_MATERIAL && transparencyLevel != SpriteTransparencyLevel.TRANSLUCENT) { + return AngelicaRenderPassConfiguration.CUTOUT_MIPPED_MATERIAL; + } + } + + return material; + } + + SpriteTransparencyLevel transparencyLevel(TextureAtlasSprite sprite) { + return ((SpriteExtension) sprite).celeritas$getTransparencyLevel(); + } +} diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/celeritas/QuadMaterialSelectorTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/celeritas/QuadMaterialSelectorTest.java new file mode 100644 index 000000000..eac4f4b39 --- /dev/null +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/celeritas/QuadMaterialSelectorTest.java @@ -0,0 +1,166 @@ +package com.gtnewhorizons.angelica.rendering.celeritas; + +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.data.AnimationMetadataSection; +import org.embeddedt.embeddium.impl.model.quad.properties.ModelQuadFacing; +import org.embeddedt.embeddium.impl.render.chunk.compile.sorting.QuadPrimitiveType; +import org.embeddedt.embeddium.impl.render.chunk.sprite.SpriteTransparencyLevel; +import org.embeddedt.embeddium.impl.render.chunk.terrain.TerrainRenderPass; +import org.embeddedt.embeddium.impl.render.chunk.terrain.material.Material; +import org.embeddedt.embeddium.impl.render.chunk.terrain.material.parameters.AlphaCutoffParameter; +import org.embeddedt.embeddium.impl.render.chunk.vertex.format.ChunkMeshFormats; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertSame; + +class QuadMaterialSelectorTest { + + private final Material[] previous = new Material[3]; + private final Map levels = new IdentityHashMap<>(); + private final QuadMaterialSelector selector = new QuadMaterialSelector() { + @Override + SpriteTransparencyLevel transparencyLevel(TextureAtlasSprite sprite) { + return levels.get(sprite); + } + }; + + private Material solid; + private Material cutout; + private Material translucent; + + @BeforeEach + void installMaterials() { + previous[0] = AngelicaRenderPassConfiguration.SOLID_MATERIAL; + previous[1] = AngelicaRenderPassConfiguration.CUTOUT_MIPPED_MATERIAL; + previous[2] = AngelicaRenderPassConfiguration.TRANSLUCENT_MATERIAL; + + solid = new Material(pass("solid", false), AlphaCutoffParameter.ZERO, true); + cutout = new Material(pass("cutout_mipped", true), AlphaCutoffParameter.HALF, true); + translucent = new Material(pass("translucent", false), AlphaCutoffParameter.ZERO, true); + + AngelicaRenderPassConfiguration.SOLID_MATERIAL = solid; + AngelicaRenderPassConfiguration.CUTOUT_MIPPED_MATERIAL = cutout; + AngelicaRenderPassConfiguration.TRANSLUCENT_MATERIAL = translucent; + } + + @AfterEach + void restoreMaterials() { + AngelicaRenderPassConfiguration.SOLID_MATERIAL = previous[0]; + AngelicaRenderPassConfiguration.CUTOUT_MIPPED_MATERIAL = previous[1]; + AngelicaRenderPassConfiguration.TRANSLUCENT_MATERIAL = previous[2]; + } + + private static TerrainRenderPass pass(String name, boolean fragmentDiscard) { + return new TerrainRenderPass(name, null, false, fragmentDiscard, false, false, ChunkMeshFormats.VANILLA_LIKE, QuadPrimitiveType.TRIANGULATED, Map.of()); + } + + private TextureAtlasSprite sprite(String name, SpriteTransparencyLevel level) { + final TextureAtlasSprite sprite = new TextureAtlasSprite(name); + levels.put(sprite, level); + return sprite; + } + + private TextureAtlasSprite animated(String name) { + final TextureAtlasSprite sprite = sprite(name, SpriteTransparencyLevel.OPAQUE); + sprite.animationMetadata = new AnimationMetadataSection(new ArrayList<>(), 16, 16, 1); + return sprite; + } + + private TextureAtlasSprite custom(String name) { + final TextureAtlasSprite sprite = new TextureAtlasSprite(name) {}; + levels.put(sprite, SpriteTransparencyLevel.OPAQUE); + return sprite; + } + + private void begin(Material material) { + selector.reset(material, false, true); + } + + private Material select(ModelQuadFacing facing, TextureAtlasSprite sprite) { + return selector.select(facing, sprite, false); + } + + @Test + void opaqueOverlayStaysWithAnimatedBaseOnTheSameFace() { + final TextureAtlasSprite base = animated("hadesX32"); + final TextureAtlasSprite corner = sprite("metalborder", SpriteTransparencyLevel.OPAQUE); + final TextureAtlasSprite edge = sprite("metalborder-ctm", SpriteTransparencyLevel.TRANSPARENT); + + begin(cutout); + assertSame(cutout, select(ModelQuadFacing.POS_Z, base)); + assertSame(cutout, select(ModelQuadFacing.POS_Z, corner)); + assertSame(cutout, select(ModelQuadFacing.POS_Z, edge)); + assertSame(cutout, select(ModelQuadFacing.POS_Z, corner)); + } + + @Test + void cachedDemotionDoesNotLeakOntoAFaceWithABase() { + final TextureAtlasSprite base = animated("hadesX32"); + final TextureAtlasSprite corner = sprite("metalborder", SpriteTransparencyLevel.OPAQUE); + + begin(cutout); + assertSame(solid, select(ModelQuadFacing.POS_X, corner)); + assertSame(cutout, select(ModelQuadFacing.POS_Z, base)); + assertSame(cutout, select(ModelQuadFacing.POS_Z, corner)); + assertSame(solid, select(ModelQuadFacing.POS_X, corner)); + } + + @Test + void opaqueOnlyFacesAreStillDemoted() { + final TextureAtlasSprite stone = sprite("stone", SpriteTransparencyLevel.OPAQUE); + + begin(cutout); + for (ModelQuadFacing facing : ModelQuadFacing.VALUES) { + assertSame(solid, select(facing, stone)); + } + } + + @Test + void vertexAlphaKeepsTranslucentAndPinsTheFace() { + final TextureAtlasSprite frame = sprite("frame", SpriteTransparencyLevel.TRANSPARENT); + + begin(translucent); + assertSame(cutout, select(ModelQuadFacing.NEG_Y, frame)); + assertSame(translucent, selector.select(ModelQuadFacing.POS_Y, frame, true)); + assertSame(translucent, select(ModelQuadFacing.POS_Y, frame)); + assertSame(cutout, select(ModelQuadFacing.NEG_Y, frame)); + } + + @Test + void undemotableSpritesKeepTheBaseMaterial() { + begin(cutout); + assertSame(cutout, select(ModelQuadFacing.POS_X, null)); + assertSame(cutout, select(ModelQuadFacing.POS_Y, animated("animated"))); + assertSame(cutout, select(ModelQuadFacing.POS_Z, custom("custom"))); + } + + @Test + void shaderPackOverrideAndDisabledOptimizationKeepTheBaseMaterial() { + final TextureAtlasSprite stone = sprite("stone", SpriteTransparencyLevel.OPAQUE); + + selector.reset(cutout, true, true); + assertSame(cutout, select(ModelQuadFacing.POS_X, stone)); + selector.reset(cutout, false, false); + assertSame(cutout, select(ModelQuadFacing.POS_X, stone)); + } + + @Test + void resetReleasesPinnedFacesAndTheSpriteCache() { + final TextureAtlasSprite base = animated("hadesX32"); + final TextureAtlasSprite corner = sprite("metalborder", SpriteTransparencyLevel.OPAQUE); + + begin(cutout); + assertSame(cutout, select(ModelQuadFacing.POS_Z, base)); + assertSame(cutout, select(ModelQuadFacing.POS_Z, corner)); + assertSame(solid, select(ModelQuadFacing.NEG_Z, corner)); + + begin(translucent); + assertSame(cutout, select(ModelQuadFacing.POS_Z, corner)); + } +}