diff --git a/src/main/java/thedarkcolour/exdeorum/compat/ClientXeiUtil.java b/src/main/java/thedarkcolour/exdeorum/compat/ClientXeiUtil.java index c5f0df04..8a0115fd 100644 --- a/src/main/java/thedarkcolour/exdeorum/compat/ClientXeiUtil.java +++ b/src/main/java/thedarkcolour/exdeorum/compat/ClientXeiUtil.java @@ -1,35 +1,45 @@ package thedarkcolour.exdeorum.compat; +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.blaze3d.vertex.VertexConsumer; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.gui.render.TextureSetup; import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.block.BlockAndTintGetter; +import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart; +import net.minecraft.client.renderer.state.gui.GuiElementRenderState; +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.core.BlockPos; import net.minecraft.core.component.DataComponentPatch; import net.minecraft.core.component.DataComponents; import net.minecraft.network.chat.Component; +import net.minecraft.util.RandomSource; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.state.BlockState; +import org.joml.Matrix3x2f; +import org.joml.Matrix4f; +import org.joml.Vector3f; +import org.joml.Vector3fc; import thedarkcolour.exdeorum.ExDeorum; import thedarkcolour.exdeorum.client.RenderUtil; import thedarkcolour.exdeorum.data.TranslationKeys; import thedarkcolour.exdeorum.material.DefaultMaterials; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + // client-only logic shared between JEI and EMI public class ClientXeiUtil { private static final ItemStack OAK_BARREL_COMPOSTING = new ItemStack(DefaultMaterials.OAK_BARREL.getItemHolder(), 1, DataComponentPatch.builder().set(DataComponents.ITEM_MODEL, ExDeorum.loc("oak_barrel_composting")).build()); public static void renderBlock(GuiGraphicsExtractor guiGraphics, BlockState state, float x, float y, float z, float scale) { - var fluidState = state.getFluidState(); - if (!fluidState.isEmpty()) { - var sprite = RenderUtil.getFluidSprite(fluidState.getType()); - guiGraphics.blitSprite(RenderPipelines.GUI_TEXTURED, sprite, Math.round(x), Math.round(y), Math.round(scale), Math.round(scale)); - return; - } - - var stack = new ItemStack(state.getBlock()); - if (!stack.isEmpty()) { - renderScaledItem(guiGraphics, stack, x, y, scale); - } + submitBlockPreview(guiGraphics, state, x, y, z, scale); } public static void renderItemWithAsterisk(GuiGraphicsExtractor graphics, ItemStack stack) { @@ -53,12 +63,189 @@ public class ClientXeiUtil { return Component.translatable(TranslationKeys.SIEVE_RECIPE_CHANCE, chance).withStyle(ChatFormatting.GRAY); } - private static void renderScaledItem(GuiGraphicsExtractor graphics, ItemStack stack, float x, float y, float scale) { - var pose = graphics.pose(); - pose.pushMatrix(); - pose.translate(x, y); - pose.scale(scale / 16f); - graphics.fakeItem(stack, 0, 0); - pose.popMatrix(); + private static void submitBlockPreview(GuiGraphicsExtractor graphics, BlockState state, float x, float y, float z, float scale) { + graphics.submitGuiElementRenderState(new BlockPreviewRenderState(new Matrix3x2f(graphics.pose()), state, x, y, z, scale)); + } + + private record BlockPreviewRenderState(Matrix3x2f pose, BlockState state, float x, float y, float z, + float scale) implements GuiElementRenderState { + private static final ScreenRectangle NO_SCISSOR = null; + + @Override + public void buildVertices(VertexConsumer consumer) { + var matrix = createBlockPreviewMatrix(x, y, z, scale); + var quads = new ArrayList(); + var fluidState = state.getFluidState(); + + if (fluidState.isEmpty()) { + var model = Minecraft.getInstance().getModelManager().getBlockStateModelSet().get(state); + var parts = new ArrayList(); + model.collectParts(BlockAndTintGetter.EMPTY, BlockPos.ZERO, state, RandomSource.create(42), parts); + + for (var part : parts) { + for (var direction : net.minecraft.core.Direction.values()) { + for (var quad : part.getQuads(direction)) { + quads.add(PreviewQuad.fromBakedQuad(quad, matrix, getBlockTint(state, quad))); + } + } + for (var quad : part.getQuads(null)) { + quads.add(PreviewQuad.fromBakedQuad(quad, matrix, getBlockTint(state, quad))); + } + } + } else { + addFluidCube(quads, matrix, fluidState.getType()); + } + + quads.sort(Comparator.comparingDouble(PreviewQuad::depth)); + for (var quad : quads) { + quad.emit(consumer, pose); + } + } + + @Override + public RenderPipeline pipeline() { + return RenderPipelines.GUI_TEXTURED; + } + + @Override + public TextureSetup textureSetup() { + AbstractTexture texture = Minecraft.getInstance().getTextureManager().getTexture(TextureAtlas.LOCATION_BLOCKS); + return TextureSetup.singleTexture(texture.getTextureView(), texture.getSampler()); + } + + @Override + public ScreenRectangle scissorArea() { + return NO_SCISSOR; + } + + @Override + public ScreenRectangle bounds() { + int minX = (int) Math.floor(x - scale); + int minY = (int) Math.floor(y - scale); + int size = (int) Math.ceil(scale * 2.0f); + return new ScreenRectangle(minX, minY, size, size).transformAxisAligned(pose); + } + } + + private record PreviewQuad(ProjectedVertex v0, ProjectedVertex v1, ProjectedVertex v2, ProjectedVertex v3, + float depth) { + private static PreviewQuad fromBakedQuad(net.minecraft.client.resources.model.geometry.BakedQuad quad, Matrix4f matrix, int color) { + var v0 = ProjectedVertex.fromBakedQuad(quad.position(0), quad.packedUV(0), matrix, color); + var v1 = ProjectedVertex.fromBakedQuad(quad.position(1), quad.packedUV(1), matrix, color); + var v2 = ProjectedVertex.fromBakedQuad(quad.position(2), quad.packedUV(2), matrix, color); + var v3 = ProjectedVertex.fromBakedQuad(quad.position(3), quad.packedUV(3), matrix, color); + float centerX = matrix.transformPosition(0.5f, 0.5f, 0.5f, new Vector3f()).x; + float quadX = (v0.x + v1.x + v2.x + v3.x) * 0.25f; + float shade = quad.materialInfo().shade() ? getGuiFaceShade(quad.direction(), quadX, centerX) : 1.0f; + return new PreviewQuad( + v0.withColor(shadeColor(v0.color, shade)), + v1.withColor(shadeColor(v1.color, shade)), + v2.withColor(shadeColor(v2.color, shade)), + v3.withColor(shadeColor(v3.color, shade)), + (v0.z + v1.z + v2.z + v3.z) * 0.25f + ); + } + + private static PreviewQuad fromSprite(Vector3fc p0, Vector3fc p1, Vector3fc p2, Vector3fc p3, Matrix4f matrix, TextureAtlasSprite sprite, int color) { + var v0 = ProjectedVertex.fromSprite(p0, matrix, sprite.getU0(), sprite.getV0(), color); + var v1 = ProjectedVertex.fromSprite(p1, matrix, sprite.getU0(), sprite.getV1(), color); + var v2 = ProjectedVertex.fromSprite(p2, matrix, sprite.getU1(), sprite.getV1(), color); + var v3 = ProjectedVertex.fromSprite(p3, matrix, sprite.getU1(), sprite.getV0(), color); + return new PreviewQuad(v0, v1, v2, v3, (v0.z + v1.z + v2.z + v3.z) * 0.25f); + } + + private void emit(VertexConsumer consumer, Matrix3x2f pose) { + v0.emit(consumer, pose); + v1.emit(consumer, pose); + v2.emit(consumer, pose); + v3.emit(consumer, pose); + } + } + + private record ProjectedVertex(float x, float y, float z, float u, float v, int color) { + private static ProjectedVertex fromBakedQuad(Vector3fc position, long packedUv, Matrix4f matrix, int color) { + var transformed = matrix.transformPosition(position, new Vector3f()); + float u = Float.intBitsToFloat((int) (packedUv >> 32)); + float v = Float.intBitsToFloat((int) packedUv); + return new ProjectedVertex(transformed.x, transformed.y, transformed.z, u, v, color); + } + + private static ProjectedVertex fromSprite(Vector3fc position, Matrix4f matrix, float u, float v, int color) { + var transformed = matrix.transformPosition(position, new Vector3f()); + return new ProjectedVertex(transformed.x, transformed.y, transformed.z, u, v, color); + } + + private void emit(VertexConsumer consumer, Matrix3x2f pose) { + consumer.addVertexWith2DPose(pose, x, y).setUv(u, v).setColor(color); + } + + private ProjectedVertex withColor(int color) { + return new ProjectedVertex(x, y, z, u, v, color); + } + } + + private static Matrix4f createBlockPreviewMatrix(float x, float y, float z, float scale) { + return new Matrix4f() + .translate(x, y, z) + .scale(-scale, -scale, -scale) + .translate(-0.5f, -0.5f, 0.0f) + .rotateX((float) Math.toRadians(-30.0f)) + .translate(0.5f, 0.0f, -0.5f) + .rotateY((float) Math.toRadians(45.0f)) + .translate(-0.5f, 0.0f, 0.5f) + .translate(0.0f, 0.0f, -1.0f); + } + + private static void addFluidCube(List quads, Matrix4f matrix, net.minecraft.world.level.material.Fluid fluid) { + var still = RenderUtil.getFluidSprite(fluid); + int color = 0xffffffff; + var level = Minecraft.getInstance().level; + if (level != null) { + color = 0xff000000 | (RenderUtil.getFluidColor(fluid, level, net.minecraft.core.BlockPos.ZERO) & 0xffffff); + } + + var p000 = new Vector3f(0.0f, 0.0f, 0.0f); + var p001 = new Vector3f(0.0f, 0.0f, 1.0f); + var p010 = new Vector3f(0.0f, 1.0f, 0.0f); + var p011 = new Vector3f(0.0f, 1.0f, 1.0f); + var p100 = new Vector3f(1.0f, 0.0f, 0.0f); + var p101 = new Vector3f(1.0f, 0.0f, 1.0f); + var p110 = new Vector3f(1.0f, 1.0f, 0.0f); + var p111 = new Vector3f(1.0f, 1.0f, 1.0f); + + quads.add(PreviewQuad.fromSprite(p010, p011, p111, p110, matrix, still, color)); + quads.add(PreviewQuad.fromSprite(p000, p100, p101, p001, matrix, still, color)); + quads.add(PreviewQuad.fromSprite(p001, p101, p111, p011, matrix, still, color)); + quads.add(PreviewQuad.fromSprite(p100, p000, p010, p110, matrix, still, color)); + quads.add(PreviewQuad.fromSprite(p000, p001, p011, p010, matrix, still, color)); + quads.add(PreviewQuad.fromSprite(p101, p100, p110, p111, matrix, still, color)); + } + + private static int getBlockTint(BlockState state, net.minecraft.client.resources.model.geometry.BakedQuad quad) { + var material = quad.materialInfo(); + if (!material.isTinted()) { + return -1; + } + var tintSource = Minecraft.getInstance().getBlockColors().getTintSource(state, material.tintIndex()); + if (tintSource == null) { + return -1; + } + return 0xff000000 | (tintSource.color(state) & 0xffffff); + } + + private static float getGuiFaceShade(net.minecraft.core.Direction direction, float quadX, float centerX) { + return switch (direction) { + case UP -> 1.0f; + case DOWN -> 0.5f; + default -> quadX < centerX ? 0.8f : 0.6f; + }; + } + + private static int shadeColor(int color, float shade) { + int alpha = color & 0xff000000; + int red = Math.min(255, Math.round(((color >> 16) & 0xff) * shade)); + int green = Math.min(255, Math.round(((color >> 8) & 0xff) * shade)); + int blue = Math.min(255, Math.round((color & 0xff) * shade)); + return alpha | red << 16 | green << 8 | blue; } }