diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d781b2d..079e295 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 with: - gradle-version: '8.8' + gradle-version: '9.4.0' - name: Build mod run: gradle build diff --git a/README.md b/README.md index 1b4b1d1..5369af9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,14 @@ # ForceRender -A minimal **client-side** Fabric mod for **Minecraft 1.21.1** that prevents armor stands and item -frames from being frustum-culled when they are within a configurable range. No commands, no tags — -just open Mod Menu, tweak the slider, and your furniture stays visible. +A minimal **client-side** Fabric mod for **Minecraft 1.21.x** that keeps custom display models +looking right: + +- **Force render** — prevents armor stands and item frames from being frustum-culled when they are + within a configurable range, so large custom models stop popping out of view at the screen edge. +- **Fix item translucency** — stops translucent items (held, worn, or framed) from disappearing or + being clipped when they sit behind water and glass. + +No commands, no tags — just open Mod Menu, flip the toggles, and your furniture stays visible. ![Preview](assets/ON_AND_OFF.gif) @@ -24,14 +30,31 @@ ForceRender mixes into `EntityRenderer#shouldRender`. For every armor stand and the configured range, it returns `true` immediately — bypassing the frustum check. Entities outside that range fall back to vanilla behaviour, so performance is only affected near the player. +## Translucent Items Behind Water / Glass + +Minecraft renders translucent **entities** (armor stands and the items they hold or wear) in a +separate pass from translucent **blocks** (water, glass). Render layers are only depth-sorted +*within* each pass, never across them, so a translucent item and a translucent block have no defined +blend order relative to each other. The result: transparent items **vanish or get clipped** the +moment they sit behind or near water or glass. + +ForceRender fixes this by intercepting the two render layers Minecraft uses to draw items in the +world — `TexturedRenderLayers#getItemTranslucentCull` (the block-atlas layer used by most items) and +`RenderLayers#itemEntityTranslucentCull` (the per-texture layer) — and returning the matching +**cutout** layer instead. Cutout is alpha-tested and writes depth like an opaque surface, so items +resolve against translucent blocks correctly. The only trade-off is the usual one for cutout: no +soft alpha fade (each pixel is either fully drawn or discarded), which is imperceptible for the vast +majority of item textures. Toggle it off if you specifically need soft transparency. + ## Settings (Mod Menu) Open **Mod Menu → ForceRender → (gear icon)** to access the config screen: | Setting | Description | Default | |---|---|---| -| **Force Render** | Enable or disable the mod entirely | Enabled | +| **Force Render** | Enable or disable the frustum-culling bypass | Enabled | | **Render Range** | Entities beyond this many blocks use vanilla culling | 32 blocks | +| **Fix Item Translucency** | Draw world items as cutout so they don't vanish behind water/glass | Enabled | Settings are saved to `.minecraft/config/forcerender.properties` and can also be edited by hand. @@ -52,7 +75,7 @@ Requirements: JDK 21 ```bash # One-time: generate the Gradle wrapper binary -gradle wrapper --gradle-version=8.8 +gradle wrapper --gradle-version=9.4.0 # Build ./gradlew build @@ -81,9 +104,11 @@ on a dedicated server. ## Compatibility -| Minecraft | Fabric Loader | Fabric API | Mod Menu | -|-----------|---------------|-----------------|----------------| -| 1.21.1 | ≥ 0.16.0 | 0.102.0+1.21.1 | ≥ 11.0 (opt.) | +| Minecraft | Fabric Loader | Fabric API | Mod Menu | +|-------------|---------------|------------|---------------| +| 1.21.1 – 1.21.11 | ≥ 0.16.0 | `*` | ≥ 11.0 (opt.) | + +> Built against 1.21.11; the JAR also loads on 1.21.1 – 1.21.10. ## License diff --git a/gradle.properties b/gradle.properties index cb95a03..b2e4fde 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,6 +13,6 @@ fabric_version=0.141.4+1.21.11 modmenu_version=17.0.0-beta.1 # Mod metadata -mod_version=1.1.0 +mod_version=1.2.0 maven_group=net.reimaginedpixel.forcerender archives_base_name=forcerender diff --git a/src/main/java/net/reimaginedpixel/forcerender/ForceRenderConfig.java b/src/main/java/net/reimaginedpixel/forcerender/ForceRenderConfig.java index 7b12a99..7b28cdc 100644 --- a/src/main/java/net/reimaginedpixel/forcerender/ForceRenderConfig.java +++ b/src/main/java/net/reimaginedpixel/forcerender/ForceRenderConfig.java @@ -12,8 +12,9 @@ public final class ForceRenderConfig { private static final Path CONFIG_FILE = FabricLoader.getInstance().getConfigDir().resolve("forcerender.properties"); - public static boolean enabled = true; - public static int renderRange = 32; // blocks; 1–256 + public static boolean enabled = true; + public static int renderRange = 32; // blocks; 1–256 + public static boolean fixTranslucency = true; // cutout-swap for translucent items private ForceRenderConfig() {} @@ -26,6 +27,7 @@ public static void load() { try (var reader = Files.newBufferedReader(CONFIG_FILE)) { props.load(reader); enabled = Boolean.parseBoolean(props.getProperty("enabled", "true")); + fixTranslucency = Boolean.parseBoolean(props.getProperty("fixTranslucency", "true")); try { renderRange = clampRange(Integer.parseInt(props.getProperty("renderRange", "32"))); } catch (NumberFormatException e) { @@ -38,8 +40,9 @@ public static void load() { public static void save() { Properties props = new Properties(); - props.setProperty("enabled", String.valueOf(enabled)); - props.setProperty("renderRange", String.valueOf(renderRange)); + props.setProperty("enabled", String.valueOf(enabled)); + props.setProperty("renderRange", String.valueOf(renderRange)); + props.setProperty("fixTranslucency", String.valueOf(fixTranslucency)); try (var writer = Files.newBufferedWriter(CONFIG_FILE)) { props.store(writer, "ForceRender — edit here or use the in-game Mod Menu settings screen"); } catch (IOException e) { diff --git a/src/main/java/net/reimaginedpixel/forcerender/ForceRenderConfigScreen.java b/src/main/java/net/reimaginedpixel/forcerender/ForceRenderConfigScreen.java index 2812d84..69754df 100644 --- a/src/main/java/net/reimaginedpixel/forcerender/ForceRenderConfigScreen.java +++ b/src/main/java/net/reimaginedpixel/forcerender/ForceRenderConfigScreen.java @@ -23,14 +23,17 @@ public class ForceRenderConfigScreen extends Screen { // Working copies — only committed to config when Done is pressed private boolean pendingEnabled; private int pendingRange; + private boolean pendingFixTranslucency; private ButtonWidget toggleButton; + private ButtonWidget translucencyButton; public ForceRenderConfigScreen(Screen parent) { super(Text.literal("ForceRender Settings")); - this.parent = parent; - this.pendingEnabled = ForceRenderConfig.enabled; - this.pendingRange = ForceRenderConfig.renderRange; + this.parent = parent; + this.pendingEnabled = ForceRenderConfig.enabled; + this.pendingRange = ForceRenderConfig.renderRange; + this.pendingFixTranslucency = ForceRenderConfig.fixTranslucency; } @Override @@ -54,6 +57,15 @@ protected void init() { WIDGET_WIDTH, WIDGET_HEIGHT)); + // ── Fix translucency toggle ──────────────────────────────────────── + translucencyButton = ButtonWidget.builder(buildTranslucencyLabel(), btn -> { + pendingFixTranslucency = !pendingFixTranslucency; + translucencyButton.setMessage(buildTranslucencyLabel()); + }) + .dimensions(centerX - WIDGET_WIDTH / 2, startY + ROW_SPACING * 2, WIDGET_WIDTH, WIDGET_HEIGHT) + .build(); + addDrawableChild(translucencyButton); + // ── Done / Cancel ────────────────────────────────────────────────── int bottomY = height - BOTTOM_MARGIN; int halfWidth = WIDGET_WIDTH / 2 - 2; @@ -105,9 +117,15 @@ private Text buildToggleLabel() { return Text.literal("Force Render: " + state); } + private Text buildTranslucencyLabel() { + String state = pendingFixTranslucency ? COLOR_ON + "ON" : COLOR_OFF + "OFF"; + return Text.literal("Fix Item Translucency: " + state); + } + private void saveAndClose() { - ForceRenderConfig.enabled = pendingEnabled; - ForceRenderConfig.renderRange = pendingRange; + ForceRenderConfig.enabled = pendingEnabled; + ForceRenderConfig.renderRange = pendingRange; + ForceRenderConfig.fixTranslucency = pendingFixTranslucency; ForceRenderConfig.save(); client.setScreen(parent); } diff --git a/src/main/java/net/reimaginedpixel/forcerender/ForceRenderMod.java b/src/main/java/net/reimaginedpixel/forcerender/ForceRenderMod.java index daaf831..4c6df1e 100644 --- a/src/main/java/net/reimaginedpixel/forcerender/ForceRenderMod.java +++ b/src/main/java/net/reimaginedpixel/forcerender/ForceRenderMod.java @@ -14,7 +14,7 @@ public class ForceRenderMod implements ClientModInitializer { @Override public void onInitializeClient() { ForceRenderConfig.load(); - LOGGER.info("[ForceRender] Loaded — enabled={}, range={}", - ForceRenderConfig.enabled, ForceRenderConfig.renderRange); + LOGGER.info("[ForceRender] Loaded — enabled={}, range={}, fixTranslucency={}", + ForceRenderConfig.enabled, ForceRenderConfig.renderRange, ForceRenderConfig.fixTranslucency); } } diff --git a/src/main/java/net/reimaginedpixel/forcerender/mixin/ItemAtlasTranslucencyMixin.java b/src/main/java/net/reimaginedpixel/forcerender/mixin/ItemAtlasTranslucencyMixin.java new file mode 100644 index 0000000..7397d33 --- /dev/null +++ b/src/main/java/net/reimaginedpixel/forcerender/mixin/ItemAtlasTranslucencyMixin.java @@ -0,0 +1,35 @@ +package net.reimaginedpixel.forcerender.mixin; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.TexturedRenderLayers; +import net.reimaginedpixel.forcerender.ForceRenderConfig; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * Companion to {@link ItemTranslucencyMixin} for standard items. + * + *

Most items in the world (dropped items, item-frame contents, and items held + * or worn by armor stands) are drawn from the block atlas via + * {@code TexturedRenderLayers.getItemTranslucentCull}, not the per-texture layer + * in {@link net.minecraft.client.render.RenderLayers}. Swapping that atlas layer + * for the matching {@code getEntityCutout} layer — same atlas texture, cutout + * instead of translucent — moves those items into the depth-writing cutout pass + * so they composite correctly against water and glass. See + * {@link ItemTranslucencyMixin} for the full rationale and trade-off. + */ +@Environment(EnvType.CLIENT) +@Mixin(TexturedRenderLayers.class) +public class ItemAtlasTranslucencyMixin { + + @Inject(method = "getItemTranslucentCull", at = @At("HEAD"), cancellable = true) + private static void forceCutoutForAtlasItems(CallbackInfoReturnable callbackInfo) { + if (ForceRenderConfig.enabled && ForceRenderConfig.fixTranslucency) { + callbackInfo.setReturnValue(TexturedRenderLayers.getEntityCutout()); + } + } +} diff --git a/src/main/java/net/reimaginedpixel/forcerender/mixin/ItemTranslucencyMixin.java b/src/main/java/net/reimaginedpixel/forcerender/mixin/ItemTranslucencyMixin.java new file mode 100644 index 0000000..40e7f56 --- /dev/null +++ b/src/main/java/net/reimaginedpixel/forcerender/mixin/ItemTranslucencyMixin.java @@ -0,0 +1,47 @@ +package net.reimaginedpixel.forcerender.mixin; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.RenderLayers; +import net.minecraft.util.Identifier; +import net.reimaginedpixel.forcerender.ForceRenderConfig; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * Fixes translucent items (held / worn / framed) that vanish or get clipped when + * they sit behind or near translucent blocks such as water and glass. + * + *

Vanilla renders translucent entities and translucent blocks in two separate + * passes and only depth-sorts render layers within each pass, never + * across them. A translucent item and a translucent block therefore have no + * defined blend order relative to one another, so the item's alpha pixels are + * composited incorrectly — most visibly they disappear behind water or glass. + * + *

{@code RenderLayers.itemEntityTranslucentCull} builds the per-texture layer + * used to draw custom-textured items in the world. Swapping it for the matching + * {@code entityCutout} layer moves those items into the cutout pass, which is + * alpha-tested and writes depth like an opaque surface, so depth resolves the + * item against translucent blocks correctly. The only trade-off is the usual one + * for cutout: no soft alpha fade (a pixel is either fully drawn or discarded), + * which is imperceptible for the overwhelming majority of item textures. + * + * @see ItemAtlasTranslucencyMixin for the block-atlas item layer. + */ +@Environment(EnvType.CLIENT) +@Mixin(RenderLayers.class) +public class ItemTranslucencyMixin { + + @Inject(method = "itemEntityTranslucentCull", at = @At("HEAD"), cancellable = true) + private static void forceCutoutForItems( + Identifier texture, + CallbackInfoReturnable callbackInfo) { + + if (ForceRenderConfig.enabled && ForceRenderConfig.fixTranslucency) { + callbackInfo.setReturnValue(RenderLayers.entityCutout(texture)); + } + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 9021423..623c662 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -4,7 +4,7 @@ "version": "${version}", "name": "ForceRender", - "description": "Prevents armor stands and item frames from being frustum-culled when they are within a configurable range, fixing large custom models that disappear at screen edges.", + "description": "Prevents armor stands and item frames from being frustum-culled when they are within a configurable range, and fixes translucent items that vanish behind water and glass. Keeps large custom models visible and correctly composited.", "authors": [ "reimaginedpixel" ], diff --git a/src/main/resources/forcerender.mixins.json b/src/main/resources/forcerender.mixins.json index 4e32398..7e132b2 100644 --- a/src/main/resources/forcerender.mixins.json +++ b/src/main/resources/forcerender.mixins.json @@ -4,7 +4,9 @@ "package": "net.reimaginedpixel.forcerender.mixin", "compatibilityLevel": "JAVA_21", "client": [ - "ForceRenderMixin" + "ForceRenderMixin", + "ItemTranslucencyMixin", + "ItemAtlasTranslucencyMixin" ], "mixins": [], "injectors": {