Draw glass on every version

1.21.6 to 1.21.10 draw glass as 1.21.11 does, without samplers, and the
glass shader imports vanilla's transforms block, whose layout differs
between them. 1.21.4 and 1.21.5 draw straight into the framebuffer,
so the game is saved before the screen blurs it, and glass is a render
layer: with a pipeline on 1.21.5 and a shader program on 1.21.4.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-09-28 21:55:52 +02:00
co-authored by claude
parent 4e00934353
commit 2b5bcf4098
7 changed files with 297 additions and 27 deletions
+1 -1
Submodule common updated: 7bc308155e...524fc36bf3
@@ -1,6 +1,20 @@
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.util.HashMap;
import java.util.Map;
@@ -17,22 +31,17 @@ import com.mojang.serialization.JsonOps;
import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.gl.Framebuffer;
import net.minecraft.client.gl.GpuSampler;
import net.minecraft.client.gl.PostEffectPipeline;
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.ProjectionMatrix2;
import net.minecraft.client.texture.AbstractTexture;
import net.minecraft.client.texture.TextureSetup;
import net.minecraft.client.util.Pool;
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 static final RenderPipeline PIPELINE = RenderPipeline.builder(RenderPipelines.POSITION_TEX_COLOR_SNIPPET)
.withLocation(Identifier.of("saturnclient", "pipeline/glass"))
@@ -52,8 +61,8 @@ public final class GlassRenderer {
private GlassRenderer() {
}
// Copies the main framebuffer and blurs the copy by blur. Call once a frame for each blur, before
// drawing glass with it.
// Copies the main framebuffer and blurs the copy by blur. Call once a frame for each blur, while
// the screen builds its GUI, before drawing glass with it.
public static void captureBackdrop(int blur) {
Framebuffer main = SaturnClient.client.getFramebuffer();
int width = main.textureWidth;
@@ -66,6 +75,8 @@ public final class GlassRenderer {
} else if (backdrop.textureWidth != width || backdrop.textureHeight != height) {
backdrop.resize(width, height);
}
//? if <1.21.11
//backdrop.getColorAttachment().setTextureFilter(FilterMode.LINEAR, false);
RenderSystem.getDevice().createCommandEncoder().copyTextureToTexture(
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
public static TextureSetup textures(GpuTextureView mask, GpuSampler maskSampler, int blur) {
return TextureSetup.of(mask, maskSampler, BACKDROPS.get(blur).getColorAttachmentView(),
RenderSystem.getSamplerCache().get(FilterMode.LINEAR));
public static TextureSetup textures(AbstractTexture mask, int blur) {
GpuTextureView backdrop = BACKDROPS.get(blur).getColorAttachmentView();
//? 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) {
@@ -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;
//?} else
//import net.minecraft.client.render.item.ItemRenderState;
//? if >=1.21.11
import net.minecraft.client.texture.AbstractTexture;
import net.minecraft.client.texture.Sprite;
import net.minecraft.client.texture.TextureSetup;
@@ -63,10 +62,8 @@ public class RenderScopeImpl implements RenderScope {
private ScissorStack scissorStack;
private int opacity = 0xFF << 24;
//? if >=1.21.11 {
// The glass blurs made for this scope's frame
private final Set<Integer> capturedBlurs = new HashSet<>();
//?}
public RenderScopeImpl(DrawContext context) {
this(context.getMatrices(), ((DrawContextAccessor) (Object) context).getState());
@@ -236,7 +233,6 @@ public class RenderScopeImpl implements RenderScope {
this.scissorStack.peekLast()));
}
//? if >=1.21.11 {
@Override
public void drawGlass(IdentifierRef mask, int x, int y, int width, int height, int blur) {
int color = getColor(0xFFFFFFFF);
@@ -250,12 +246,11 @@ public class RenderScopeImpl implements RenderScope {
AbstractTexture maskTexture = SaturnClient.client.getTextureManager().getTexture((Identifier) (Object) mask);
this.state.addSimpleElement(new TexturedQuadGuiElementRenderState(
GlassRenderer.PIPELINE,
GlassRenderer.textures(maskTexture.getGlTextureView(), maskTexture.getSampler(), blur),
GlassRenderer.textures(maskTexture, blur),
new Matrix3x2f(this.matrices.stack),
x, y, x + width, y + height, 0, 1, 0, 1, color,
this.scissorStack.peekLast()));
}
//?}
// Scissor management
@Override
@@ -395,7 +390,9 @@ public class RenderScopeImpl implements RenderScope {
//?} else {
/*import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import org.jetbrains.annotations.Nullable;
@@ -448,6 +445,8 @@ public class RenderScopeImpl implements RenderScope {
public VertexConsumerProvider.Immediate vertexConsumers;
private ScissorStack scissorStack = new ScissorStack();
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;
// ----------------------------
@@ -687,6 +686,33 @@ public class RenderScopeImpl implements RenderScope {
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
// ----------------------------
@@ -97,6 +97,10 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
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(
Identifier.ofVanilla("blur"),
DefaultFramebufferSet.MAIN_ONLY);
@@ -1,13 +1,10 @@
#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 {
mat4 ModelViewMat;
vec4 ColorModulator;
vec3 ModelOffset;
mat4 TextureMat;
};
// Vanilla's own, since its layout changes between versions (1.21.11 dropped LineWidth)
#moj_import <minecraft:dynamictransforms.glsl>
// The panel's rounded rectangle, the same texture its background is drawn with
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;
}