Add GUI scale and glass on every version #40

Merged
selimaj-dev merged 11 commits from glass-blur into master 2026-09-28 20:02:50 +00:00
7 changed files with 297 additions and 27 deletions
Showing only changes of commit 2b5bcf4098 - Show all commits
+1 -1
Submodule common updated: 7bc308155e...524fc36bf3
@@ -1,6 +1,20 @@
package org.saturnclient.impl.ui; package org.saturnclient.impl.ui;
//? if >=1.21.11 { // Glass: a copy of the game behind the screen, blurred, drawn through each panel's rounded rectangle
// texture as a mask. There's a copy for each blur strength in use (menus and widgets have their own),
// blurred with vanilla's blur at that radius, so it holds the world or panorama without the screen's
// own animated background blur.
//
// The GUI rewrites split this into three implementations:
// - 1.21.6+: the GUI is drawn after the screen builds it, so the copy is taken from the main
// framebuffer while the screen builds its GUI, and glass is a GUI element with its own pipeline
// (shaders/core/glass.fsh). The blur is vanilla's effect rebuilt with the radius set.
// - 1.21.5: the screen draws straight into the framebuffer, blurring it first, so the game is saved
// before the blur (saveBackground). Glass is a render layer with its own pipeline
// (shaders/core/glass_pre_1_21_6.fsh), and the blur takes the radius as it runs.
// - 1.21.4: as 1.21.5, with GL framebuffers copied by blitting, and a shader program
// (shaders/core/glass.json) instead of a pipeline.
//? if >=1.21.6 {
import java.io.Reader; import java.io.Reader;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -17,22 +31,17 @@ import com.mojang.serialization.JsonOps;
import org.saturnclient.saturnclient.SaturnClient; import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.gl.Framebuffer; import net.minecraft.client.gl.Framebuffer;
import net.minecraft.client.gl.GpuSampler;
import net.minecraft.client.gl.PostEffectPipeline; import net.minecraft.client.gl.PostEffectPipeline;
import net.minecraft.client.gl.PostEffectProcessor; import net.minecraft.client.gl.PostEffectProcessor;
import net.minecraft.client.gl.RenderPipelines; import net.minecraft.client.gl.RenderPipelines;
import net.minecraft.client.gl.SimpleFramebuffer; import net.minecraft.client.gl.SimpleFramebuffer;
import net.minecraft.client.render.DefaultFramebufferSet; import net.minecraft.client.render.DefaultFramebufferSet;
import net.minecraft.client.render.ProjectionMatrix2; import net.minecraft.client.render.ProjectionMatrix2;
import net.minecraft.client.texture.AbstractTexture;
import net.minecraft.client.texture.TextureSetup; import net.minecraft.client.texture.TextureSetup;
import net.minecraft.client.util.Pool; import net.minecraft.client.util.Pool;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
// Glass: a copy of the game behind the screen, blurred, drawn through each panel's rounded rectangle
// texture (shaders/core/glass.fsh). There's a copy for each blur strength in use (menus and widgets
// have their own), made while the screen builds its GUI, before the GUI is drawn, so it holds the
// world or panorama alone, without the screen's own animated background blur.
// TODO: glass before 1.21.11 (#23). Those versions draw translucent panels without it.
public final class GlassRenderer { public final class GlassRenderer {
public static final RenderPipeline PIPELINE = RenderPipeline.builder(RenderPipelines.POSITION_TEX_COLOR_SNIPPET) public static final RenderPipeline PIPELINE = RenderPipeline.builder(RenderPipelines.POSITION_TEX_COLOR_SNIPPET)
.withLocation(Identifier.of("saturnclient", "pipeline/glass")) .withLocation(Identifier.of("saturnclient", "pipeline/glass"))
@@ -52,8 +61,8 @@ public final class GlassRenderer {
private GlassRenderer() { private GlassRenderer() {
} }
// Copies the main framebuffer and blurs the copy by blur. Call once a frame for each blur, before // Copies the main framebuffer and blurs the copy by blur. Call once a frame for each blur, while
// drawing glass with it. // the screen builds its GUI, before drawing glass with it.
public static void captureBackdrop(int blur) { public static void captureBackdrop(int blur) {
Framebuffer main = SaturnClient.client.getFramebuffer(); Framebuffer main = SaturnClient.client.getFramebuffer();
int width = main.textureWidth; int width = main.textureWidth;
@@ -66,6 +75,8 @@ public final class GlassRenderer {
} else if (backdrop.textureWidth != width || backdrop.textureHeight != height) { } else if (backdrop.textureWidth != width || backdrop.textureHeight != height) {
backdrop.resize(width, height); backdrop.resize(width, height);
} }
//? if <1.21.11
//backdrop.getColorAttachment().setTextureFilter(FilterMode.LINEAR, false);
RenderSystem.getDevice().createCommandEncoder().copyTextureToTexture( RenderSystem.getDevice().createCommandEncoder().copyTextureToTexture(
main.getColorAttachment(), backdrop.getColorAttachment(), 0, 0, 0, 0, 0, width, height); main.getColorAttachment(), backdrop.getColorAttachment(), 0, 0, 0, 0, 0, width, height);
@@ -78,9 +89,12 @@ public final class GlassRenderer {
} }
// The mask, then the copy blurred by blur, as the glass pipeline samples them // The mask, then the copy blurred by blur, as the glass pipeline samples them
public static TextureSetup textures(GpuTextureView mask, GpuSampler maskSampler, int blur) { public static TextureSetup textures(AbstractTexture mask, int blur) {
return TextureSetup.of(mask, maskSampler, BACKDROPS.get(blur).getColorAttachmentView(), GpuTextureView backdrop = BACKDROPS.get(blur).getColorAttachmentView();
RenderSystem.getSamplerCache().get(FilterMode.LINEAR)); //? if >=1.21.11
return TextureSetup.of(mask.getGlTextureView(), mask.getSampler(), backdrop, RenderSystem.getSamplerCache().get(FilterMode.LINEAR));
//? if <1.21.11
//return TextureSetup.of(mask.getGlTextureView(), backdrop);
} }
private static PostEffectProcessor getBlur(int blur) { private static PostEffectProcessor getBlur(int blur) {
@@ -119,4 +133,193 @@ public final class GlassRenderer {
} }
} }
} }
//?} //?} else if >=1.21.5 {
/*import java.util.HashMap;
import java.util.Map;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.textures.FilterMode;
import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.gl.Framebuffer;
import net.minecraft.client.gl.PostEffectProcessor;
import net.minecraft.client.gl.RenderPipelines;
import net.minecraft.client.gl.SimpleFramebuffer;
import net.minecraft.client.render.DefaultFramebufferSet;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.RenderPhase;
import net.minecraft.client.util.Pool;
import net.minecraft.util.Identifier;
public final class GlassRenderer {
private static final RenderPipeline PIPELINE = RenderPipeline.builder(RenderPipelines.POSITION_TEX_COLOR_SNIPPET)
.withLocation(Identifier.of("saturnclient", "pipeline/glass"))
.withFragmentShader(Identifier.of("saturnclient", "core/glass_pre_1_21_6"))
.withSampler("Sampler1")
.build();
private static final Identifier BLUR = Identifier.ofVanilla("blur");
// For the blur's intermediate target, freed once unused for a few frames
private static final Pool POOL = new Pool(3);
private static final Map<Integer, SimpleFramebuffer> BACKDROPS = new HashMap<>();
// By mask and blur
private static final Map<String, RenderLayer> LAYERS = new HashMap<>();
// The game behind the screen before the screen blurs it, which glass is blurred from
private static SimpleFramebuffer background;
private GlassRenderer() {
}
// Saves the main framebuffer for glass. Call each frame before the screen blurs its background.
public static void saveBackground() {
background = copy(SaturnClient.client.getFramebuffer(), background, "Saturn glass background");
}
// Copies the saved background and blurs the copy by blur. Call once a frame for each blur, before
// drawing glass with it.
public static void captureBackdrop(int blur) {
Framebuffer from = background != null ? background : SaturnClient.client.getFramebuffer();
SimpleFramebuffer backdrop = copy(from, BACKDROPS.get(blur), "Saturn glass " + blur);
BACKDROPS.put(blur, backdrop);
PostEffectProcessor processor = SaturnClient.client.getShaderLoader().loadPostEffect(BLUR,
DefaultFramebufferSet.MAIN_ONLY);
if (processor != null) {
processor.render(backdrop, POOL, pass -> pass.setUniform("Radius", (float) blur));
}
POOL.decrementLifespan();
}
// Draws glass through mask with the copy blurred by blur
public static RenderLayer getLayer(Identifier mask, int blur) {
return LAYERS.computeIfAbsent(mask + " " + blur, key -> RenderLayer.of("saturn_glass", 786432, PIPELINE,
RenderLayer.MultiPhaseParameters.builder()
.texture(new RenderPhase.TextureBase(() -> {
RenderSystem.setShaderTexture(0,
SaturnClient.client.getTextureManager().getTexture(mask).getGlTexture());
RenderSystem.setShaderTexture(1, BACKDROPS.get(blur).getColorAttachment());
}, () -> {
}))
.build(false)));
}
// Copies from into to, made or resized to match
private static SimpleFramebuffer copy(Framebuffer from, SimpleFramebuffer to, String name) {
int width = from.textureWidth;
int height = from.textureHeight;
if (to == null) {
to = new SimpleFramebuffer(name, width, height, false);
} else if (to.textureWidth != width || to.textureHeight != height) {
to.resize(width, height);
}
to.getColorAttachment().setTextureFilter(FilterMode.LINEAR, false);
RenderSystem.getDevice().createCommandEncoder().copyTextureToTexture(
from.getColorAttachment(), to.getColorAttachment(), 0, 0, 0, 0, 0, width, height);
return to;
}
}
*///?} else {
/*import java.util.HashMap;
import java.util.Map;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL30;
import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.systems.RenderSystem;
import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.gl.Defines;
import net.minecraft.client.gl.Framebuffer;
import net.minecraft.client.gl.PostEffectProcessor;
import net.minecraft.client.gl.ShaderProgramKey;
import net.minecraft.client.gl.SimpleFramebuffer;
import net.minecraft.client.render.DefaultFramebufferSet;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.RenderPhase;
import net.minecraft.client.render.VertexFormat;
import net.minecraft.client.render.VertexFormats;
import net.minecraft.client.util.Pool;
import net.minecraft.util.Identifier;
public final class GlassRenderer {
private static final ShaderProgramKey PROGRAM = new ShaderProgramKey(Identifier.of("saturnclient", "core/glass"),
VertexFormats.POSITION_TEXTURE_COLOR, Defines.EMPTY);
private static final Identifier BLUR = Identifier.ofVanilla("blur");
// For the blur's intermediate target, freed once unused for a few frames
private static final Pool POOL = new Pool(3);
private static final Map<Integer, SimpleFramebuffer> BACKDROPS = new HashMap<>();
// By mask and blur
private static final Map<String, RenderLayer> LAYERS = new HashMap<>();
// The game behind the screen before the screen blurs it, which glass is blurred from
private static SimpleFramebuffer background;
private GlassRenderer() {
}
// Saves the main framebuffer for glass. Call each frame before the screen blurs its background.
public static void saveBackground() {
background = copy(SaturnClient.client.getFramebuffer(), background);
}
// Copies the saved background and blurs the copy by blur. Call once a frame for each blur, before
// drawing glass with it.
public static void captureBackdrop(int blur) {
Framebuffer from = background != null ? background : SaturnClient.client.getFramebuffer();
SimpleFramebuffer backdrop = copy(from, BACKDROPS.get(blur));
BACKDROPS.put(blur, backdrop);
PostEffectProcessor processor = SaturnClient.client.getShaderLoader().loadPostEffect(BLUR,
DefaultFramebufferSet.MAIN_ONLY);
if (processor != null) {
processor.setUniforms("Radius", blur);
processor.render(backdrop, POOL);
}
POOL.decrementLifespan();
SaturnClient.client.getFramebuffer().beginWrite(false);
}
// Draws glass through mask with the copy blurred by blur
public static RenderLayer getLayer(Identifier mask, int blur) {
return LAYERS.computeIfAbsent(mask + " " + blur, key -> RenderLayer.of("saturn_glass",
VertexFormats.POSITION_TEXTURE_COLOR, VertexFormat.DrawMode.QUADS, 786432,
RenderLayer.MultiPhaseParameters.builder()
.texture(new RenderPhase.TextureBase(() -> {
RenderSystem.setShaderTexture(0, mask);
RenderSystem.setShaderTexture(1, BACKDROPS.get(blur).getColorAttachment());
}, () -> {
}))
.program(new RenderPhase.ShaderProgram(PROGRAM))
.transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY)
.build(false)));
}
// Copies from into to, made or resized to match
private static SimpleFramebuffer copy(Framebuffer from, SimpleFramebuffer to) {
int width = from.textureWidth;
int height = from.textureHeight;
if (to == null) {
to = new SimpleFramebuffer(width, height, false);
} else if (to.textureWidth != width || to.textureHeight != height) {
to.resize(width, height);
}
to.setTexFilter(GL11.GL_LINEAR);
GlStateManager._glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, from.fbo);
GlStateManager._glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, to.fbo);
GlStateManager._glBlitFrameBuffer(0, 0, width, height, 0, 0, width, height, GL11.GL_COLOR_BUFFER_BIT,
GL11.GL_NEAREST);
SaturnClient.client.getFramebuffer().beginWrite(false);
return to;
}
}
*///?}
@@ -42,7 +42,6 @@ import net.minecraft.client.render.entity.state.EntityRenderState;
import net.minecraft.client.render.item.KeyedItemRenderState; import net.minecraft.client.render.item.KeyedItemRenderState;
//?} else //?} else
//import net.minecraft.client.render.item.ItemRenderState; //import net.minecraft.client.render.item.ItemRenderState;
//? if >=1.21.11
import net.minecraft.client.texture.AbstractTexture; import net.minecraft.client.texture.AbstractTexture;
import net.minecraft.client.texture.Sprite; import net.minecraft.client.texture.Sprite;
import net.minecraft.client.texture.TextureSetup; import net.minecraft.client.texture.TextureSetup;
@@ -63,10 +62,8 @@ public class RenderScopeImpl implements RenderScope {
private ScissorStack scissorStack; private ScissorStack scissorStack;
private int opacity = 0xFF << 24; private int opacity = 0xFF << 24;
//? if >=1.21.11 {
// The glass blurs made for this scope's frame // The glass blurs made for this scope's frame
private final Set<Integer> capturedBlurs = new HashSet<>(); private final Set<Integer> capturedBlurs = new HashSet<>();
//?}
public RenderScopeImpl(DrawContext context) { public RenderScopeImpl(DrawContext context) {
this(context.getMatrices(), ((DrawContextAccessor) (Object) context).getState()); this(context.getMatrices(), ((DrawContextAccessor) (Object) context).getState());
@@ -236,7 +233,6 @@ public class RenderScopeImpl implements RenderScope {
this.scissorStack.peekLast())); this.scissorStack.peekLast()));
} }
//? if >=1.21.11 {
@Override @Override
public void drawGlass(IdentifierRef mask, int x, int y, int width, int height, int blur) { public void drawGlass(IdentifierRef mask, int x, int y, int width, int height, int blur) {
int color = getColor(0xFFFFFFFF); int color = getColor(0xFFFFFFFF);
@@ -250,12 +246,11 @@ public class RenderScopeImpl implements RenderScope {
AbstractTexture maskTexture = SaturnClient.client.getTextureManager().getTexture((Identifier) (Object) mask); AbstractTexture maskTexture = SaturnClient.client.getTextureManager().getTexture((Identifier) (Object) mask);
this.state.addSimpleElement(new TexturedQuadGuiElementRenderState( this.state.addSimpleElement(new TexturedQuadGuiElementRenderState(
GlassRenderer.PIPELINE, GlassRenderer.PIPELINE,
GlassRenderer.textures(maskTexture.getGlTextureView(), maskTexture.getSampler(), blur), GlassRenderer.textures(maskTexture, blur),
new Matrix3x2f(this.matrices.stack), new Matrix3x2f(this.matrices.stack),
x, y, x + width, y + height, 0, 1, 0, 1, color, x, y, x + width, y + height, 0, 1, 0, 1, color,
this.scissorStack.peekLast())); this.scissorStack.peekLast()));
} }
//?}
// Scissor management // Scissor management
@Override @Override
@@ -395,7 +390,9 @@ public class RenderScopeImpl implements RenderScope {
//?} else { //?} else {
/*import java.util.ArrayDeque; /*import java.util.ArrayDeque;
import java.util.Deque; import java.util.Deque;
import java.util.HashSet;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -448,6 +445,8 @@ public class RenderScopeImpl implements RenderScope {
public VertexConsumerProvider.Immediate vertexConsumers; public VertexConsumerProvider.Immediate vertexConsumers;
private ScissorStack scissorStack = new ScissorStack(); private ScissorStack scissorStack = new ScissorStack();
private int opacity = 255 << 24; private int opacity = 255 << 24;
// The glass blurs made for this scope's frame
private final Set<Integer> capturedBlurs = new HashSet<>();
private final ItemRenderState itemRenderState; private final ItemRenderState itemRenderState;
// ---------------------------- // ----------------------------
@@ -687,6 +686,33 @@ public class RenderScopeImpl implements RenderScope {
matrices.pop(); matrices.pop();
} }
@Override
public void drawGlass(IdentifierRef mask, int x, int y, int width, int height, int blur) {
int color = getColor(0xFFFFFFFF);
if (mask == null || color >>> 24 == 0)
return;
if (capturedBlurs.add(blur)) {
GlassRenderer.captureBackdrop(blur);
}
// In quarter units, as drawTexturedQuad draws
int x1 = x * 4;
int x2 = (x + width) * 4;
int y1 = y * 4;
int y2 = (y + height) * 4;
matrices.push();
matrices.scale(0.25f, 0.25f, 1.0f);
Matrix4f matrix4f = matrices.peek().getPositionMatrix();
VertexConsumer vertexConsumer = vertexConsumers.getBuffer(GlassRenderer.getLayer((Identifier) (Object) mask, blur));
vertexConsumer.vertex(matrix4f, x1, y1, 0).texture(0, 0).color(color);
vertexConsumer.vertex(matrix4f, x1, y2, 0).texture(0, 1).color(color);
vertexConsumer.vertex(matrix4f, x2, y2, 0).texture(1, 1).color(color);
vertexConsumer.vertex(matrix4f, x2, y1, 0).texture(1, 0).color(color);
matrices.pop();
}
// ---------------------------- // ----------------------------
// Items // Items
// ---------------------------- // ----------------------------
@@ -97,6 +97,10 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
ROTATING_PANORAMA_RENDERER.render(context, this.width, this.height, screen.backgroundOpacity, delta); ROTATING_PANORAMA_RENDERER.render(context, this.width, this.height, screen.backgroundOpacity, delta);
} }
// Glass is blurred from the game as it is before this screen blurs it.
context.draw();
GlassRenderer.saveBackground();
PostEffectProcessor postEffectProcessor = this.client.getShaderLoader().loadPostEffect( PostEffectProcessor postEffectProcessor = this.client.getShaderLoader().loadPostEffect(
Identifier.ofVanilla("blur"), Identifier.ofVanilla("blur"),
DefaultFramebufferSet.MAIN_ONLY); DefaultFramebufferSet.MAIN_ONLY);
@@ -1,13 +1,10 @@
#version 330 #version 330
// Saturn's glass: the blurred game behind the screen, cut to a panel's shape. // Saturn's glass: the blurred game behind the screen, cut to a panel's shape. For 1.21.6 and
// later; glass_pre_1_21_6.fsh (with glass.json, the program 1.21.4 loads) is for older versions.
layout(std140) uniform DynamicTransforms { // Vanilla's own, since its layout changes between versions (1.21.11 dropped LineWidth)
mat4 ModelViewMat; #moj_import <minecraft:dynamictransforms.glsl>
vec4 ColorModulator;
vec3 ModelOffset;
mat4 TextureMat;
};
// The panel's rounded rectangle, the same texture its background is drawn with // The panel's rounded rectangle, the same texture its background is drawn with
uniform sampler2D Sampler0; uniform sampler2D Sampler0;
@@ -0,0 +1,13 @@
{
"vertex": "minecraft:core/position_tex_color",
"fragment": "saturnclient:core/glass_pre_1_21_6",
"samplers": [
{ "name": "Sampler0" },
{ "name": "Sampler1" }
],
"uniforms": [
{ "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
{ "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
{ "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] }
]
}
@@ -0,0 +1,27 @@
#version 150
// Saturn's glass (see glass.fsh), for 1.21.4 and 1.21.5, which set uniforms one by one instead of in
// blocks.
uniform vec4 ColorModulator;
// The panel's rounded rectangle, the same texture its background is drawn with
uniform sampler2D Sampler0;
// The game behind the screen, blurred, the size of the framebuffer
uniform sampler2D Sampler1;
in vec2 texCoord0;
in vec4 vertexColor;
out vec4 fragColor;
void main() {
float mask = texture(Sampler0, texCoord0).a * vertexColor.a;
if (mask == 0.0) {
discard;
}
// The blurred copy lines up with the framebuffer being drawn to.
vec2 screenUv = gl_FragCoord.xy / vec2(textureSize(Sampler1, 0));
fragColor = vec4(texture(Sampler1, screenUv).rgb, mask) * ColorModulator;
}