feat: add built-in menu blur based on Blur by tterrag

This commit is contained in:
Ranzu committed 2026-10-23 12:00:11 +02:00
1 parent 8af9340be6
commit f959648090
11 files changed
+332 -1

No files matched your search

+3 -1
View File
@@ -17,7 +17,7 @@
* HUD caching
* Custom fonts and text effects
* MCPatcher - connected textures, custom item textures, custom colors, random mobs, better skies
* Bundled NotFine, zoom, FPS reducer
* Bundled NotFine, zoom, FPS reducer, menu blur
## Status
* OpenGL + Celeritas terrain - Default
@@ -129,5 +129,7 @@ Targets GL 3.3+ core profile
* FFP vertex shader generation inspired by ffvertex_prog.c (Copyright 2007 VMware, Inc.)
* FFP fragment shader generation inspired by ff_fragment_shader.c (Copyright 2007, 2009 VMware, Inc. / 2010-2011 Intel Corporation)
* Fog parameter optimization inspired by st_nir_lower_fog.c (Copyright 2023 Google LLC) and prog_statevars.c (Copyright 1999-2007 Brian Paul)
* Blur
* tterrag for [Blur](https://github.com/tterrag1098/Blur) - Menu blur based on Blur under the MIT license
* RGSS Terrain Texture Filtering
* Indiesoft for [defold-sharp-sprite](https://github.com/indiesoftby/defold-sharp-sprite), used as the reference implementation under the MIT license
@@ -0,0 +1,25 @@
The menu blur in this package is based on [Blur](https://github.com/tterrag1098/Blur), used under the following license:
```
MIT License
Copyright (c) 2017 tterrag
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
@@ -0,0 +1,195 @@
package com.gtnewhorizons.angelica.blur;
import com.gtnewhorizons.angelica.client.rendering.GlUniformFloat2v;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.StateSet;
import com.gtnewhorizons.angelica.render.PanoramaRenderer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.shader.Framebuffer;
import org.apache.commons.lang3.ArrayUtils;
import org.embeddedt.embeddium.impl.gl.shader.GlProgram;
import org.embeddedt.embeddium.impl.gl.shader.ShaderBindingContext;
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformFloat;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL30;
import java.nio.ByteBuffer;
/**
* Blurs the world behind open GUIs and fades in a configurable background gradient.
* Based on Blur by tterrag (https://github.com/tterrag1098/Blur) under the MIT license, see LICENSE-Blur.md.
* Reimplemented on top of Angelica's own blur shader instead of a vanilla post-processing shader.
*/
public class MenuBlur {
private static final Minecraft mc = Minecraft.getMinecraft();
private static GlProgram<BlurUniforms> blurProgram;
private static StateSet allState;
private static int emptyVao;
// The blur runs on half resolution copies of the screen, which is much cheaper and looks the same once blurred
private static int horizontalFbo, horizontalTexture, verticalFbo, verticalTexture;
private static int sourceWidth, sourceHeight, halfWidth, halfHeight;
private static boolean blurring;
private static long fadeStartMillis;
/** Called right after the world has been drawn, before the HUD and the open GUI. */
public static void renderBlur() {
final boolean shouldBlur = shouldBlur();
if (shouldBlur && !blurring) {
fadeStartMillis = System.currentTimeMillis();
}
blurring = shouldBlur;
if (!blurring) return;
// The radius grows with the fade, so the blur gets stronger over the first moments
final float radius = (float) Math.floor(AngelicaConfig.menuBlurRadius * getProgress());
if (radius < 1.0F) return;
final Framebuffer mainFramebuffer = mc.getFramebuffer();
if (blurProgram == null) init();
final int previousProgram = GLStateManager.getActiveProgram();
final int previousVao = GLStateManager.glGetInteger(GL30.GL_VERTEX_ARRAY_BINDING);
final int previousFilter = mainFramebuffer.framebufferFilter;
final int stateDepth = GLStateManager.pushState(allState);
try {
ensureBlurTargets(mainFramebuffer.framebufferWidth, mainFramebuffer.framebufferHeight);
GLStateManager.disableBlend();
GLStateManager.disableDepthTest();
GLStateManager.glDepthMask(false);
GLStateManager.glColorMask(true, true, true, true);
GLStateManager.disableCull();
GLStateManager.disableScissorTest();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
blurProgram.bind();
GLStateManager.glBindVertexArray(emptyVao);
final BlurUniforms uniforms = blurProgram.getInterface();
// Each half resolution pixel covers two screen pixels, so half the radius gives the same strength
uniforms.radius.setFloat(radius * 0.5F);
// Horizontal pass, shrinking the screen to half size. Linear filtering makes every
// half size pixel average the four screen pixels it covers instead of picking one.
mainFramebuffer.setFramebufferFilter(GL11.GL_LINEAR);
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, horizontalFbo);
GLStateManager.glViewport(0, 0, halfWidth, halfHeight);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, mainFramebuffer.framebufferTexture);
uniforms.direction.set(1.0F / halfWidth, 0.0F);
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
// Vertical pass, still at half size
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, verticalFbo);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, horizontalTexture);
uniforms.direction.set(0.0F, 1.0F / halfHeight);
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
// Stretch the result back over the screen. A radius of 0 makes the blur shader a plain copy.
mainFramebuffer.bindFramebuffer(true);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, verticalTexture);
uniforms.radius.setFloat(0.0F);
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
} finally {
mainFramebuffer.setFramebufferFilter(previousFilter);
GLStateManager.glBindVertexArray(previousVao);
GLStateManager.glUseProgram(previousProgram);
GLStateManager.popStateTo(stateDepth);
mainFramebuffer.bindFramebuffer(true);
}
}
/** Replaces one of the two vanilla GUI background gradient colors while the blur is showing. */
public static int getBackgroundColor(int vanillaColor, String configuredColor) {
if (!blurring) return vanillaColor;
final int color;
try {
color = Integer.parseUnsignedInt(configuredColor, 16);
} catch (NumberFormatException e) {
return vanillaColor;
}
final float progress = getProgress();
final int alpha = (int) ((color >>> 24) * progress);
final int red = (int) ((color >> 16 & 0xFF) * progress);
final int green = (int) ((color >> 8 & 0xFF) * progress);
final int blue = (int) ((color & 0xFF) * progress);
return alpha << 24 | red << 16 | green << 8 | blue;
}
private static boolean shouldBlur() {
return AngelicaConfig.enableMenuBlur
&& mc.theWorld != null
&& mc.currentScreen != null
&& !ArrayUtils.contains(AngelicaConfig.menuBlurExcludedGuis, mc.currentScreen.getClass().getName());
}
private static float getProgress() {
if (AngelicaConfig.menuBlurFadeTime <= 0) return 1.0F;
return Math.min((System.currentTimeMillis() - fadeStartMillis) / (float) AngelicaConfig.menuBlurFadeTime, 1.0F);
}
private static void init() {
blurProgram = PanoramaRenderer.loadProgram("angelica:menu_blur", "angelica:panorama_blur", "angelica:panorama_blur", BlurUniforms::new);
allState = StateSet.forMask(GL11.GL_ALL_ATTRIB_BITS);
emptyVao = GLStateManager.glGenVertexArrays();
}
private static void ensureBlurTargets(int width, int height) {
if (horizontalFbo != 0 && width == sourceWidth && height == sourceHeight) return;
if (horizontalFbo != 0) {
GLStateManager.glDeleteFramebuffers(horizontalFbo);
GLStateManager.glDeleteFramebuffers(verticalFbo);
GLStateManager.glDeleteTextures(horizontalTexture);
GLStateManager.glDeleteTextures(verticalTexture);
}
sourceWidth = width;
sourceHeight = height;
halfWidth = Math.max((width + 1) / 2, 1);
halfHeight = Math.max((height + 1) / 2, 1);
horizontalTexture = createTexture(halfWidth, halfHeight);
horizontalFbo = createFramebuffer(horizontalTexture);
verticalTexture = createTexture(halfWidth, halfHeight);
verticalFbo = createFramebuffer(verticalTexture);
}
private static int createTexture(int width, int height) {
final int texture = GLStateManager.glGenTextures();
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture);
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, width, height, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0);
return texture;
}
private static int createFramebuffer(int colorTexture) {
final int fbo = GLStateManager.glGenFramebuffers();
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo);
GLStateManager.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, colorTexture, 0);
final int status = GLStateManager.glCheckFramebufferStatus(GL30.GL_FRAMEBUFFER);
if (status != GL30.GL_FRAMEBUFFER_COMPLETE) {
throw new RuntimeException("Menu blur FBO incomplete: 0x" + Integer.toHexString(status));
}
return fbo;
}
private static class BlurUniforms {
final GlUniformFloat2v direction;
final GlUniformFloat radius;
BlurUniforms(ShaderBindingContext ctx) {
direction = ctx.bindUniform("u_Direction", GlUniformFloat2v::new);
radius = ctx.bindUniform("u_Radius", GlUniformFloat::new);
}
}
}
@@ -276,6 +276,32 @@ public class AngelicaConfig {
@Config.RequiresMcRestart
public static boolean enableZoom;
@Config.Comment("Blur the world behind open GUIs.")
@Config.DefaultBoolean(false)
public static boolean enableMenuBlur;
@Config.Comment("How strong the menu blur is, in pixels.")
@Config.DefaultInt(12)
@Config.RangeInt(min = 1, max = 100)
public static int menuBlurRadius;
@Config.Comment("How long the menu blur takes to fade in, in milliseconds.")
@Config.DefaultInt(200)
@Config.RangeInt(min = 0, max = 2000)
public static int menuBlurFadeTime;
@Config.Comment("GUI classes that never get the menu blur.")
@Config.DefaultStringList({"net.minecraft.client.gui.GuiChat"})
public static String[] menuBlurExcludedGuis;
@Config.Comment("Top color of the GUI background gradient while the menu blur is showing, as ARGB hex.")
@Config.DefaultString("75000000")
public static String menuBlurGradientTopColor;
@Config.Comment("Bottom color of the GUI background gradient while the menu blur is showing, as ARGB hex.")
@Config.DefaultString("75000000")
public static String menuBlurGradientBottomColor;
@Config.Comment("Optimizes in-world item rendering")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
@@ -617,6 +617,14 @@ public enum Mixins implements IMixins {
"angelica.zoom.MixinMouseFilter"
)),
ANGELICA_MENU_BLUR(new MixinBuilder("Blur the world behind open GUIs")
.setPhase(Phase.EARLY)
.addExcludedMod(TargetedMod.BLUR)
.addClientMixins(
"angelica.blur.MixinEntityRenderer_MenuBlur",
"angelica.blur.MixinGuiScreen_MenuBlur"
)),
ANGELICA_FPS_REDUCER(new MixinBuilder("Reduce frame rate and volume while unfocused, minimized, or idle")
.setPhase(Phase.EARLY)
.addClientMixins(
@@ -9,6 +9,7 @@ public enum TargetedMod implements ITargetMod {
ARCHAICFIX("org.embeddedt.archaicfix.ArchaicCore", "archaicfix"),
BACKHAND("xonin.backhand.coremod.BackhandLoadingPlugin", "backhand"),
BLUR("com.tterrag.blur.BlurPlugin", "blur"),
CAMPFIRE_BACKPORT(null, "campfirebackport"),
CHISEL(null, "chisel"),
COFHCORE( "cofh.asm.LoadingPlugin", "CoFHCore"),
@@ -847,6 +847,29 @@ public class SodiumGameOptionPages {
.build());
}
groups.add(OptionGroup.createBuilder()
.add(OptionImpl.createBuilder(boolean.class, angelicaOpts)
.setName(I18n.format("options.angelica.menuBlur"))
.setTooltip(I18n.format("options.angelica.menuBlur.tooltip"))
.setControl(TickBoxControl::new)
.setImpact(OptionImpact.LOW)
.setBinding((opts, value) -> AngelicaConfig.enableMenuBlur = value, opts -> AngelicaConfig.enableMenuBlur)
.build())
.add(OptionImpl.createBuilder(int.class, angelicaOpts)
.setName(I18n.format("options.angelica.menuBlurRadius"))
.setTooltip(I18n.format("options.angelica.menuBlurRadius.tooltip"))
.setControl(option -> new SliderControl(option, 1, 100, 1, ControlValueFormatter.number()))
.setImpact(OptionImpact.LOW)
.setBinding((opts, value) -> AngelicaConfig.menuBlurRadius = value, opts -> AngelicaConfig.menuBlurRadius)
.build())
.add(OptionImpl.createBuilder(int.class, angelicaOpts)
.setName(I18n.format("options.angelica.menuBlurFadeTime"))
.setTooltip(I18n.format("options.angelica.menuBlurFadeTime.tooltip"))
.setControl(option -> new SliderControl(option, 0, 2000, 50, ControlValueFormatter.milliseconds()))
.setBinding((opts, value) -> AngelicaConfig.menuBlurFadeTime = value, opts -> AngelicaConfig.menuBlurFadeTime)
.build())
.build());
return new OptionPage(I18n.format("sodium.options.pages.appearance"), ImmutableList.copyOf(groups));
}
@@ -43,6 +43,10 @@ public interface ControlValueFormatter {
return (v) -> v + "s";
}
static ControlValueFormatter milliseconds() {
return (v) -> v + "ms";
}
static ControlValueFormatter quantity(String name) {
return (v) -> I18n.format(name, v);
}
@@ -391,6 +391,12 @@ options.angelica.disablef3=Disable F3 Additions
options.angelica.disablef3.tooltip=Disables Angelica's additions to the F3 debug screen.
options.angelica.verbosef3=Verbose F3
options.angelica.verbosef3.tooltip=Shows developer counters (fixed-function shaders, streaming, TESR batching, GPU transfer) on the F3 screen. Toggle in game with F3+V.
options.angelica.menuBlur=Menu Blur
options.angelica.menuBlur.tooltip=Blurs the world behind open menus like the inventory or the pause menu. Chat is never blurred. Excluded menus and background colors can be changed in the Angelica config.
options.angelica.menuBlurRadius=Menu Blur Strength
options.angelica.menuBlurRadius.tooltip=How strong the menu blur is. Higher values blur more.
options.angelica.menuBlurFadeTime=Menu Blur Fade Time
options.angelica.menuBlurFadeTime.tooltip=How long the menu blur takes to fade in after a menu opens. 0 shows it instantly.
angelica.debug.verbose.on=Verbose F3: shown
angelica.debug.verbose.off=Verbose F3: hidden
options.angelica.texteffects.rgb_colors=RGB Colors
@@ -0,0 +1,19 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.blur;
import com.gtnewhorizons.angelica.blur.MenuBlur;
import net.minecraft.client.renderer.EntityRenderer;
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.CallbackInfo;
@Mixin(EntityRenderer.class)
public class MixinEntityRenderer_MenuBlur {
@Inject(
method = "updateCameraAndRender",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/EntityRenderer;renderWorld(FJ)V", shift = At.Shift.AFTER))
private void angelica$blurWorldBehindGui(float partialTicks, CallbackInfo ci) {
MenuBlur.renderBlur();
}
}
@@ -0,0 +1,22 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.blur;
import com.gtnewhorizons.angelica.blur.MenuBlur;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import net.minecraft.client.gui.GuiScreen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.Constant;
import org.spongepowered.asm.mixin.injection.ModifyConstant;
@Mixin(GuiScreen.class)
public class MixinGuiScreen_MenuBlur {
@ModifyConstant(method = "drawWorldBackground", constant = @Constant(intValue = 0xC0101010))
private int angelica$blurGradientTop(int original) {
return MenuBlur.getBackgroundColor(original, AngelicaConfig.menuBlurGradientTopColor);
}
@ModifyConstant(method = "drawWorldBackground", constant = @Constant(intValue = 0xD0101010))
private int angelica$blurGradientBottom(int original) {
return MenuBlur.getBackgroundColor(original, AngelicaConfig.menuBlurGradientBottomColor);
}
}