Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 33 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}

Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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<RenderLayer> callbackInfo) {
if (ForceRenderConfig.enabled && ForceRenderConfig.fixTranslucency) {
callbackInfo.setReturnValue(TexturedRenderLayers.getEntityCutout());
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Vanilla renders translucent entities and translucent blocks in two separate
* passes and only depth-sorts render layers <em>within</em> 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.
*
* <p>{@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<RenderLayer> callbackInfo) {

if (ForceRenderConfig.enabled && ForceRenderConfig.fixTranslucency) {
callbackInfo.setReturnValue(RenderLayers.entityCutout(texture));
}
}
}
2 changes: 1 addition & 1 deletion src/main/resources/fabric.mod.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
],
Expand Down
4 changes: 3 additions & 1 deletion src/main/resources/forcerender.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"package": "net.reimaginedpixel.forcerender.mixin",
"compatibilityLevel": "JAVA_21",
"client": [
"ForceRenderMixin"
"ForceRenderMixin",
"ItemTranslucencyMixin",
"ItemAtlasTranslucencyMixin"
],
"mixins": [],
"injectors": {
Expand Down
Loading