Compare commits

...
28 Commits
Author SHA1 Message Date
selimaj-dev 37db94649f Merge pull request 'Bump the version to 0.1.4-beta' (#46) from release-0.1.4-beta into master
Release / build (release) Successful in 4m36s
Reviewed-on: #46
2026-09-29 16:37:19 +00:00
selimaj-devandclaude 896f4cccc0 Bump the version to 0.1.4-beta
Check / compile (pull_request) Successful in 2m29s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 18:37:03 +02:00
selimaj-dev de3c554e63 Merge pull request 'Hide the cloak on torso-curving emotes and point common at #12' (#45) from release-fixes into master
Reviewed-on: #45
2026-09-29 16:35:28 +00:00
selimaj-devandclaude 8417427059 Point common at the None cloak fix and the sidebar and dock order
Check / compile (pull_request) Successful in 2m29s
Picks up common#12.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 18:35:14 +02:00
selimaj-devandclaude ba5a32fb34 Hide the cloak during emotes that curve the torso
A flat cloak can't follow an arched back, so on the twerk it cut through the player. When the
emote being played bends the torso more than 10 degrees at any point, the cloak is hidden for the
whole emote; hiding it only while the back is curved would make it flicker. Each emote is checked
once and cached. Emotes that bend the torso less still hang the cloak from the leaning chest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 18:34:34 +02:00
selimaj-dev 8b1c94151a Merge pull request 'Use emotes 0.1.1 and point common at the new emotes' (#44) from new-emotes into master
Reviewed-on: #44
2026-09-29 16:03:15 +00:00
selimaj-devandclaude 3623024cce Use emotes 0.1.1 and point common at the new emotes
Check / compile (pull_request) Successful in 2m30s
Emotes 0.1.1 bends the torso the right way round. common now has seven more emotes and a twerk
re-exported for that fix (common#11), so the two move together.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 18:02:50 +02:00
selimaj-dev 2dcbe0302e Merge pull request 'Point common at the twerk emote' (#43) from twerk-emote into master
Reviewed-on: #43
2026-09-29 15:04:46 +00:00
selimaj-devandclaude 502bddd720 Point common at the twerk emote
Check / compile (pull_request) Successful in 2m39s
Picks up the twerk emote and the removal of the front flip (common#10).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 17:01:52 +02:00
selimaj-devandclaude 0c22fee8c0 Point common at the opaque theme upgrade
Release / build (release) Successful in 5m26s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 23:12:35 +02:00
selimaj-dev f69f6eb883 Merge pull request 'Bump the version to 0.1.3-beta' (#42) from release-0.1.3-beta into master
Reviewed-on: #42
2026-09-28 21:02:25 +00:00
selimaj-devandclaude cebd9aec96 Bump the version to 0.1.3-beta
Check / compile (pull_request) Successful in 3m0s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 23:02:09 +02:00
selimaj-dev 2554a84950 Merge pull request 'Keep hats and cloaks on the posed model, and fix resources on 1.21.4 and 1.21.5' (#41) from cosmetics-follow-emotes into master
Reviewed-on: #41
2026-09-28 21:01:06 +00:00
selimaj-devandclaude 49bbdff418 Keep hats and cloaks on the posed head and body
Check / compile (pull_request) Successful in 2m57s
The hat only took the head's rotation from 1.21.5, and the cloak sat at
a fixed spot with a hard-coded crouch, so crouching and emotes left them
where the head and body had been. Both now follow the whole player (the
root, which emotes like the backflip turn) and then the head or body, as
vanilla places head items, and the cloak leans with the chest when an
emote bends the torso.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 23:00:40 +02:00
selimaj-devandclaude 8995de9067 Stop the glass shader importing what older versions lack
1.21.4 and 1.21.5 read every shader's imports while loading resources,
and dynamictransforms.glsl only exists from 1.21.6, so resources failed
to load there. The glass shader declares the transforms block itself
again, with LineWidth behind a define the pipeline sets before 1.21.11.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 23:00:40 +02:00
selimaj-dev 18b74212ca Merge pull request 'Add GUI scale and glass on every version' (#40) from glass-blur into master
Reviewed-on: #40
2026-09-28 20:02:49 +00:00
selimaj-devandclaude 06190d0ef9 Point common at its master merge commit
Check / compile (pull_request) Successful in 3m53s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 22:01:43 +02:00
selimaj-devandclaude 254a37d757 Skip glass and blur work that draws nothing
Before 1.21.6 the game is only saved for glass when glass can be drawn,
and a background blur of 0 no longer runs the blur. Every version frees
the copies (and from 1.21.6 the blurs) of glass strengths no longer set,
instead of keeping one per strength ever used.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 21:57:10 +02:00
selimaj-devandclaude 2b5bcf4098 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]>
2026-09-28 21:55:52 +02:00
selimaj-devandclaude 4e00934353 Blur each glass strength separately on 1.21.11
Every blur strength in use gets its own copy of the game, blurred with
vanilla's blur at that radius instead of the animated menu blur.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 21:35:23 +02:00
selimaj-devandclaude c524451704 Draw glass behind translucent panels on 1.21.11
A copy of the game behind the screen is blurred once a frame and drawn
through each panel's rounded rectangle texture by a glass pipeline.
Older versions keep blurring the whole background.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 20:43:44 +02:00
selimaj-devandclaude e26ce6d076 Point common at the theme alpha fix
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 19:56:24 +02:00
selimaj-devandclaude da3dfaee66 Point common at the faster color picker
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 19:45:04 +02:00
selimaj-devandclaude 463335856f Point common at the title screen polish
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 19:39:39 +02:00
selimaj-devandclaude 3315926f7c Point common at the title screen corners
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 19:20:33 +02:00
selimaj-devandclaude 68d3fb6062 Point common at the title screen layout
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 19:09:10 +02:00
selimaj-devandclaude b108cc1f4a Scale Saturn's screens by their GUI scale setting
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 18:59:47 +02:00
selimaj-dev 167010380e updated screenshot 2026-09-27 19:48:09 +02:00
11 changed files with 606 additions and 23 deletions
+1 -1
Submodule common updated: ef0ead6bf8...b0d75af8c4
+2 -2
View File
@@ -6,10 +6,10 @@ org.gradle.caching=true
# IntelliJ IDEA is not yet fully compatible with configuration cache, see: https://github.com/FabricMC/fabric-loom/issues/1349
org.gradle.configuration-cache=false
mod_version=0.1.2-beta
mod_version=0.1.4-beta
# Saturn's emote library (git.selimaj.dev/saturnclientmc/emotes)
emotes_version=0.1.0
emotes_version=0.1.1
# Loom, for every Minecraft version (read in settings.gradle.kts).
loom_version=1.17.21
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 591 KiB

@@ -1,18 +1,28 @@
package org.saturnclient.impl.cosmetics;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import org.joml.Vector4f;
import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.config.Config;
import org.saturnclient.cosmetics.Cloaks;
import org.saturnclient.emotes.core.Animation;
import org.saturnclient.emotes.core.Bone;
import org.saturnclient.emotes.core.bend.LimbBend;
import org.saturnclient.emotes.core.bend.Vec3;
import org.saturnclient.emotes.fabric.access.BendablePart;
import org.saturnclient.impl.cosmetics.utils.ShaderUtils;
import org.saturnclient.saturnclient.SaturnRenderState;
import net.minecraft.client.model.ModelPart;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.VertexConsumer;
//? if >=1.21.9 {
@@ -33,7 +43,6 @@ import net.minecraft.item.ItemStack;
import net.minecraft.item.equipment.EquipmentAsset;
import net.minecraft.registry.RegistryKey;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.RotationAxis;
import net.minecraft.util.math.Vec3d;
/**
@@ -383,6 +392,50 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
}
}
/** A torso bend past this curves the back more than a flat cloak can follow. */
private static final float MAX_CLOAK_BEND = (float) Math.toRadians(10);
/** Whether each animation bends the torso past that at any point. */
private static final Map<Animation, Boolean> BENDS_TORSO = Collections.synchronizedMap(new WeakHashMap<>());
/**
* Whether the player is playing an emote that curves the torso (a twerk, the griddy). The cloak
* would cut through the arched back, so it's hidden for the whole emote rather than only while
* the back is curved, which would flicker.
*/
private static boolean playingTorsoBend(SaturnPlayer player) {
return org.saturnclient.emotes.fabric.Emotes.playing(player.uuid)
.map(playing -> BENDS_TORSO.computeIfAbsent(playing.animation(), CloakFeatureRenderer::bendsTorso))
.orElse(false);
}
private static boolean bendsTorso(Animation animation) {
for (float tick = 0; tick <= animation.length(); tick += 0.5f) {
if (animation.sample(tick).get(Bone.BODY).filter(body -> Math.abs(body.bend()) > MAX_CLOAK_BEND).isPresent()) {
return true;
}
}
return false;
}
/**
* An emote that bends the torso a little leans the chest from the hips, so the cloak hangs from
* where the top of the back ends up, turned as much as the torso is there.
*/
private static void followBend(MatrixStack matrices, ModelPart body) {
LimbBend bend = ((BendablePart) (Object) body).saturnEmotes$getBend();
if (bend == null || bend.isStraight()) {
return;
}
// The top of the torso, in the body's own pixels
float[] top = new float[3];
bend.bendPoint(0, 0, 0, top);
matrices.translate(top[0] / 16.0f, top[1] / 16.0f, top[2] / 16.0f);
Vec3 axis = bend.axis();
matrices.multiply(new Quaternionf().rotationAxis(bend.angleAt(0), axis.x(), axis.y(), axis.z()));
}
// Shift array left and insert new value at the start
private void updateSegmentValues(float value) {
// Shift all elements one position to the right (from end to start)
@@ -416,7 +469,7 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
//?} else
//SaturnPlayer player = SaturnPlayer.get(state.name);
if (player == null) {
if (player == null || playingTorsoBend(player)) {
return;
}
@@ -436,17 +489,24 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
matrices.push();
// On the back: the whole player (which emotes like the backflip turn), then the body's position
// and turn (which crouching and emotes move), then the chest's lean when an emote bends the torso
PlayerEntityModel model = this.getContextModel();
//? if >=1.21.5 {
model.getRootPart().applyTransform(matrices);
model.body.applyTransform(matrices);
//?} else {
/*model.getRootPart().rotate(matrices);
model.body.rotate(matrices);
*///?}
followBend(matrices, model.body);
matrices.translate(0.0f, 0.0f, 0.12f);
if (this.hasCustomModelForLayer(state.equippedChestStack, LayerType.HUMANOID)) {
matrices.translate(0.0F, -0.053125F, 0.06875F);
}
if (state.isInSneakingPose) {
matrices.translate(0, 0.16f, 0.0f);
matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(29.0f));
}
long now = System.currentTimeMillis();
if (now - lastUpdate >= 20) {
float velX = Math.min(0.8f, state.field_53537 / 108.0f);
@@ -1,6 +1,5 @@
package org.saturnclient.impl.cosmetics;
import org.joml.Quaternionf;
import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.impl.cosmetics.obj.MtlLoader;
import org.saturnclient.impl.cosmetics.obj.ObjModel;
@@ -16,7 +15,6 @@ import net.minecraft.client.render.entity.feature.FeatureRendererContext;
import net.minecraft.client.render.entity.model.PlayerEntityModel;
import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.client.model.ModelPart;
public class HatFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> {
public HatFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context) {
@@ -41,11 +39,16 @@ public class HatFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState,
matrices.push();
// On the head, as vanilla places head items: the whole player (which emotes like the backflip
// turn), then the head's position, turn and scale (which crouching and emotes move)
PlayerEntityModel model = this.getContextModel();
//? if >=1.21.5 {
ModelPart head = this.getContextModel().head;
matrices.multiply(new Quaternionf().rotationZYX(head.roll, head.yaw, head.pitch));
//?} else
//this.getContextModel().head.rotate(matrices);
model.getRootPart().applyTransform(matrices);
model.head.applyTransform(matrices);
//?} else {
/*model.getRootPart().rotate(matrices);
model.head.rotate(matrices);
*///?}
ObjModel.cosmetic("hat", player.hat).render(MtlLoader.cosmetic(player.hat),
matrices, output, light,
@@ -0,0 +1,380 @@
package org.saturnclient.impl.ui;
// 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;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.textures.FilterMode;
import com.mojang.blaze3d.textures.GpuTextureView;
import com.mojang.serialization.JsonOps;
import org.saturnclient.config.Config;
import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.gl.Framebuffer;
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;
public final class GlassRenderer {
public static final RenderPipeline PIPELINE = RenderPipeline.builder(RenderPipelines.POSITION_TEX_COLOR_SNIPPET)
.withLocation(Identifier.of("saturnclient", "pipeline/glass"))
.withFragmentShader(Identifier.of("saturnclient", "core/glass"))
.withSampler("Sampler1")
// The transforms block ends with LineWidth before 1.21.11 (see glass.fsh).
//? if <1.21.11
//.withShaderDefine("HAS_LINE_WIDTH")
.build();
private static final Identifier BLUR = Identifier.ofVanilla("post_effect/blur.json");
private static final ProjectionMatrix2 PROJECTION = new ProjectionMatrix2("saturn glass", 0.1f, 1000f, false);
// For the blurs' intermediate targets, freed once unused for a few frames
private static final Pool POOL = new Pool(3);
// By blur strength. A blur that failed to load is kept as null, so it isn't retried every frame.
private static final Map<Integer, PostEffectProcessor> BLURS = new HashMap<>();
private static final Map<Integer, SimpleFramebuffer> BACKDROPS = new HashMap<>();
private GlassRenderer() {
}
// 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;
int height = main.textureHeight;
SimpleFramebuffer backdrop = BACKDROPS.get(blur);
if (backdrop == null) {
backdrop = new SimpleFramebuffer("Saturn glass " + blur, width, height, false);
BACKDROPS.put(blur, backdrop);
} 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);
PostEffectProcessor processor = getBlur(blur);
if (processor != null) {
processor.render(backdrop, POOL);
}
POOL.decrementLifespan();
release(blur);
}
// The mask, then the copy blurred by blur, as the glass pipeline samples them
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);
}
// Frees the copies and blurs of strengths that are no longer set, besides kept
private static void release(int kept) {
BACKDROPS.entrySet().removeIf(entry -> {
boolean unused = entry.getKey() != kept && !isSet(entry.getKey());
if (unused) {
entry.getValue().delete();
}
return unused;
});
BLURS.entrySet().removeIf(entry -> {
boolean unused = entry.getKey() != kept && !isSet(entry.getKey());
if (unused && entry.getValue() != null) {
entry.getValue().close();
}
return unused;
});
}
private static boolean isSet(int blur) {
return blur == Config.menuBlur.value || blur == Config.widgetBlur.value;
}
private static PostEffectProcessor getBlur(int blur) {
if (!BLURS.containsKey(blur)) {
BLURS.put(blur, loadBlur(blur));
}
return BLURS.get(blur);
}
// Vanilla's blur, with its radius set to blur. Vanilla's own leaves it at 0, which makes the shader
// read the menu blur setting (Saturn's animated background blur while a Saturn screen is open).
private static PostEffectProcessor loadBlur(int blur) {
try (Reader reader = SaturnClient.client.getResourceManager().getResourceOrThrow(BLUR).getReader()) {
JsonElement json = JsonParser.parseReader(reader);
setRadius(json, blur);
PostEffectPipeline pipeline = PostEffectPipeline.CODEC.parse(JsonOps.INSTANCE, json).getOrThrow();
return PostEffectProcessor.parseEffect(pipeline, SaturnClient.client.getTextureManager(),
DefaultFramebufferSet.MAIN_ONLY, Identifier.of("saturnclient", "glass_blur_" + blur), PROJECTION);
} catch (Exception e) {
SaturnClient.LOGGER.error("Couldn't load the glass blur", e);
return null;
}
}
// Sets every "Radius" uniform in the effect
private static void setRadius(JsonElement json, int radius) {
if (json.isJsonArray()) {
json.getAsJsonArray().forEach(element -> setRadius(element, radius));
} else if (json.isJsonObject()) {
JsonObject object = json.getAsJsonObject();
if (object.has("name") && object.get("name").getAsString().equals("Radius")) {
object.addProperty("value", (float) radius);
}
object.entrySet().forEach(entry -> setRadius(entry.getValue(), radius));
}
}
}
//?} 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.config.Config;
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();
release(blur);
}
// 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)));
}
// Frees the copies of strengths that are no longer set, besides kept
private static void release(int kept) {
BACKDROPS.entrySet().removeIf(entry -> {
boolean unused = entry.getKey() != kept && entry.getKey() != Config.menuBlur.value
&& entry.getKey() != Config.widgetBlur.value;
if (unused) {
entry.getValue().delete();
}
return unused;
});
}
// 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.config.Config;
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();
release(blur);
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)));
}
// Frees the copies of strengths that are no longer set, besides kept
private static void release(int kept) {
BACKDROPS.entrySet().removeIf(entry -> {
boolean unused = entry.getKey() != kept && entry.getKey() != Config.menuBlur.value
&& entry.getKey() != Config.widgetBlur.value;
if (unused) {
entry.getValue().delete();
}
return unused;
});
}
// 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;
}
}
*///?}
@@ -5,7 +5,9 @@ package org.saturnclient.impl.ui;
//? if >=1.21.6 {
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix3x2f;
@@ -40,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;
@@ -61,6 +62,8 @@ public class RenderScopeImpl implements RenderScope {
private ScissorStack scissorStack;
private int opacity = 0xFF << 24;
// 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());
@@ -230,6 +233,25 @@ public class RenderScopeImpl implements RenderScope {
this.scissorStack.peekLast()));
}
@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);
}
AbstractTexture maskTexture = SaturnClient.client.getTextureManager().getTexture((Identifier) (Object) mask);
this.state.addSimpleElement(new TexturedQuadGuiElementRenderState(
GlassRenderer.PIPELINE,
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
public void enableScissor(int x1, int y1, int x2, int y2) {
@@ -368,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;
@@ -421,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;
// ----------------------------
@@ -660,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
// ----------------------------
@@ -3,6 +3,7 @@ package org.saturnclient.impl.ui;
import java.time.Duration;
import java.time.Instant;
import org.saturnclient.config.Config;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.SaturnScreen;
import org.saturnclient.ui.SaturnScreen.ScreenProvider;
@@ -86,8 +87,10 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
}
long elapsed = Duration.between(screen.start, Instant.now()).toMillis();
mouseX *= 2;
mouseY *= 2;
screen.updateScale();
mouseX = (int) (mouseX / screen.scale);
mouseY = (int) (mouseY / screen.scale);
// From 1.21.6 the background, blur included, is drawn in renderBackground.
//? if <1.21.6 {
@@ -95,12 +98,18 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
ROTATING_PANORAMA_RENDERER.render(context, this.width, this.height, screen.backgroundOpacity, delta);
}
PostEffectProcessor postEffectProcessor = this.client.getShaderLoader().loadPostEffect(
// Glass is blurred from the game as it is before this screen blurs it.
if (Config.usesGlass()) {
context.draw();
GlassRenderer.saveBackground();
}
float radius = screen.getBackgroundBlur();
PostEffectProcessor postEffectProcessor = radius < 1 ? null : this.client.getShaderLoader().loadPostEffect(
Identifier.ofVanilla("blur"),
DefaultFramebufferSet.MAIN_ONLY);
if (postEffectProcessor != null) {
float radius = screen.getBackgroundBlur();
//? if >=1.21.5
postEffectProcessor.render(this.client.getFramebuffer(), this.pool, pass -> pass.setUniform("Radius", radius));
//? if <1.21.5
@@ -245,11 +254,11 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
@Override
public int getWidth() {
return width * 2;
return width;
}
@Override
public int getHeight() {
return height * 2;
return height;
}
}
@@ -0,0 +1,38 @@
#version 330
// 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.
// Vanilla's transforms, laid out as the version has them: 1.21.6 to 1.21.10 end with LineWidth, which
// 1.21.11 dropped (GlassRenderer defines HAS_LINE_WIDTH there). Not imported: the include only exists
// from 1.21.6, and 1.21.4 and 1.21.5 still read every shader's imports while loading resources.
layout(std140) uniform DynamicTransforms {
mat4 ModelViewMat;
vec4 ColorModulator;
vec3 ModelOffset;
mat4 TextureMat;
#ifdef HAS_LINE_WIDTH
float LineWidth;
#endif
};
// 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;
}
@@ -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;
}