Skip to content

Commit 5a45157

Browse files
committed
Custom Shader Background Support
1 parent 6dcb7c5 commit 5a45157

6 files changed

Lines changed: 578 additions & 0 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright (c) 2014-2026 Wurst-Imperium and contributors.
3+
*
4+
* This source code is subject to the terms of the GNU General Public
5+
* License, version 3. If a copy of the GPL was not distributed with this
6+
* file, You can obtain one at: https://www.gnu.org/licenses/gpl-3.0.txt
7+
*/
8+
package net.wurstclient.mixin;
9+
10+
import java.util.Map;
11+
12+
import org.spongepowered.asm.mixin.Mixin;
13+
import org.spongepowered.asm.mixin.injection.At;
14+
import org.spongepowered.asm.mixin.injection.ModifyVariable;
15+
16+
import com.google.common.collect.ImmutableMap;
17+
import com.mojang.blaze3d.shaders.ShaderType;
18+
19+
import net.minecraft.client.renderer.ShaderManager;
20+
import net.minecraft.resources.Identifier;
21+
import net.minecraft.server.packs.resources.Resource;
22+
import net.wurstclient.util.ShadertoyBackgroundManager;
23+
24+
@Mixin(ShaderManager.class)
25+
public abstract class ShaderManagerMixin
26+
{
27+
@ModifyVariable(
28+
method = "loadShader(Lnet/minecraft/resources/Identifier;Lnet/minecraft/server/packs/resources/Resource;Lcom/mojang/blaze3d/shaders/ShaderType;Ljava/util/Map;Lcom/google/common/collect/ImmutableMap$Builder;)V",
29+
at = @At("HEAD"),
30+
argsOnly = true,
31+
ordinal = 0)
32+
private static Resource replaceTitleShadertoyShader(Resource resource,
33+
Identifier id, Resource originalResource, ShaderType shaderType,
34+
Map<Identifier, Resource> includes, ImmutableMap.Builder<?, ?> builder)
35+
{
36+
return ShadertoyBackgroundManager.getCustomShaderResource(id)
37+
.orElse(resource);
38+
}
39+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
* Copyright (c) 2014-2026 Wurst-Imperium and contributors.
3+
*
4+
* This source code is subject to the terms of the GNU General Public
5+
* License, version 3. If a copy of the GPL was not distributed with this
6+
* file, You can obtain one at: https://www.gnu.org/licenses/gpl-3.0.txt
7+
*/
8+
package net.wurstclient.options;
9+
10+
import net.minecraft.client.gui.GuiGraphicsExtractor;
11+
import net.minecraft.client.gui.components.Button;
12+
import net.minecraft.client.gui.components.EditBox;
13+
import net.minecraft.client.gui.components.MultiLineEditBox;
14+
import net.minecraft.client.gui.screens.Screen;
15+
import net.minecraft.network.chat.Component;
16+
import net.minecraft.util.CommonColors;
17+
import net.wurstclient.WurstClient;
18+
import net.wurstclient.other_features.WurstOptionsOtf;
19+
import net.wurstclient.settings.TextFieldSetting;
20+
import net.wurstclient.util.ShadertoyBackgroundManager;
21+
import net.wurstclient.util.WurstColors;
22+
23+
public final class CustomShadertoyBackgroundScreen extends Screen
24+
{
25+
private final Screen prevScreen;
26+
private EditBox urlBox;
27+
private MultiLineEditBox codeBox;
28+
private String status = "";
29+
private boolean loading;
30+
31+
public CustomShadertoyBackgroundScreen(Screen prevScreen)
32+
{
33+
super(Component.literal("Import Shadertoy Background"));
34+
this.prevScreen = prevScreen;
35+
}
36+
37+
@Override
38+
protected void init()
39+
{
40+
WurstOptionsOtf options =
41+
WurstClient.INSTANCE.getOtfs().wurstOptionsOtf;
42+
TextFieldSetting urlSetting =
43+
options.getTitleScreenShadertoyUrlSetting();
44+
45+
int boxWidth = Math.min(760, width - 40);
46+
int left = width / 2 - boxWidth / 2;
47+
int urlY = 58;
48+
49+
urlBox = new EditBox(font, left, urlY, boxWidth, 20,
50+
Component.literal("Shadertoy URL"));
51+
urlBox.setMaxLength(512);
52+
urlBox.setValue(urlSetting.getValue());
53+
urlBox.setHint(
54+
Component.literal("https://www.shadertoy.com/view/Nfc3RM"));
55+
urlBox.setResponder(urlSetting::setValue);
56+
addRenderableWidget(urlBox);
57+
58+
int editorY = urlY + 48;
59+
int editorHeight = Math.max(120, height - editorY - 96);
60+
codeBox = MultiLineEditBox.builder().setX(left).setY(editorY)
61+
.setPlaceholder(Component.literal(
62+
"Paste raw Shadertoy code here, including mainImage(...)."))
63+
.build(font, boxWidth, editorHeight,
64+
Component.literal("Shadertoy code"));
65+
codeBox.setCharacterLimit(262144);
66+
codeBox.setValue(ShadertoyBackgroundManager.loadRawShader());
67+
addRenderableWidget(codeBox);
68+
69+
int buttonY = editorY + editorHeight + 10;
70+
int gap = 6;
71+
int buttonWidth = 112;
72+
int totalWidth = buttonWidth * 4 + gap * 3;
73+
int buttonX = width / 2 - totalWidth / 2;
74+
75+
addRenderableWidget(
76+
Button.builder(Component.literal("Load URL"), b -> loadUrl())
77+
.bounds(buttonX, buttonY, buttonWidth, 20).build());
78+
79+
addRenderableWidget(
80+
Button.builder(Component.literal("Load Paste"), b -> loadPaste())
81+
.bounds(buttonX + (buttonWidth + gap), buttonY, buttonWidth, 20)
82+
.build());
83+
84+
addRenderableWidget(Button
85+
.builder(Component.literal("Use Built-in"), b -> useBuiltIn())
86+
.bounds(buttonX + (buttonWidth + gap) * 2, buttonY, buttonWidth, 20)
87+
.build());
88+
89+
addRenderableWidget(Button
90+
.builder(Component.literal("Back"),
91+
b -> minecraft.gui.setScreen(prevScreen))
92+
.bounds(buttonX + (buttonWidth + gap) * 3, buttonY, buttonWidth, 20)
93+
.build());
94+
95+
status = ShadertoyBackgroundManager.hasCustomShader()
96+
? "Custom shader cache is active." : "Using built-in shader.";
97+
}
98+
99+
private void loadUrl()
100+
{
101+
if(loading)
102+
return;
103+
104+
String url = urlBox.getValue().trim();
105+
if(url.isEmpty())
106+
{
107+
status = "Paste a Shadertoy URL first.";
108+
return;
109+
}
110+
111+
loading = true;
112+
status = "Downloading and converting shader...";
113+
Thread worker = new Thread(() -> {
114+
String result;
115+
try
116+
{
117+
result = ShadertoyBackgroundManager.importFromUrl(url);
118+
}catch(Exception e)
119+
{
120+
result = "Failed: " + e.getMessage();
121+
}
122+
123+
String finalResult = result;
124+
minecraft.execute(() -> {
125+
loading = false;
126+
status = finalResult;
127+
if(!finalResult.startsWith("Failed"))
128+
codeBox
129+
.setValue(ShadertoyBackgroundManager.loadRawShader());
130+
});
131+
}, "Wurst Shadertoy Importer");
132+
worker.setDaemon(true);
133+
worker.start();
134+
}
135+
136+
private void loadPaste()
137+
{
138+
if(loading)
139+
return;
140+
141+
String source = codeBox.getValue();
142+
if(source.isBlank())
143+
{
144+
status = "Paste Shadertoy code first.";
145+
return;
146+
}
147+
148+
loading = true;
149+
status = "Converting pasted shader...";
150+
Thread worker = new Thread(() -> {
151+
String result;
152+
try
153+
{
154+
result = ShadertoyBackgroundManager.importFromSource(source);
155+
}catch(Exception e)
156+
{
157+
result = "Failed: " + e.getMessage();
158+
}
159+
160+
String finalResult = result;
161+
minecraft.execute(() -> {
162+
loading = false;
163+
status = finalResult;
164+
});
165+
}, "Wurst Shadertoy Paste Importer");
166+
worker.setDaemon(true);
167+
worker.start();
168+
}
169+
170+
private void useBuiltIn()
171+
{
172+
try
173+
{
174+
ShadertoyBackgroundManager.clearCustomShader();
175+
status = "Switched back to built-in shader.";
176+
}catch(Exception e)
177+
{
178+
status = "Failed to clear custom shader: " + e.getMessage();
179+
}
180+
}
181+
182+
@Override
183+
public void onClose()
184+
{
185+
minecraft.gui.setScreen(prevScreen);
186+
}
187+
188+
@Override
189+
public void extractRenderState(GuiGraphicsExtractor context, int mouseX,
190+
int mouseY, float partialTicks)
191+
{
192+
context.fillGradient(0, 0, width, height, 0xDA10131B, 0xE0121B29);
193+
194+
context.centeredText(font, "Import Shadertoy Background", width / 2, 16,
195+
CommonColors.WHITE);
196+
context.centeredText(font,
197+
"Use a URL with an API key, or paste raw single-pass mainImage() code below.",
198+
width / 2, 30, CommonColors.LIGHT_GRAY);
199+
200+
context.text(font, "Shadertoy URL", urlBox.getX(), urlBox.getY() - 12,
201+
CommonColors.LIGHT_GRAY);
202+
context.text(font, "Raw Shadertoy Code", codeBox.getX(),
203+
codeBox.getY() - 12, CommonColors.LIGHT_GRAY);
204+
205+
for(var renderable : renderables)
206+
renderable.extractRenderState(context, mouseX, mouseY,
207+
partialTicks);
208+
209+
int statusColor = status.startsWith("Failed") ? WurstColors.LIGHT_RED
210+
: CommonColors.LIGHT_GRAY;
211+
context.centeredText(font, status, width / 2, height - 42, statusColor);
212+
context.centeredText(font,
213+
"URL import uses -Dwurst.shadertoyApiKey=<your key> when set; paste import works without a key.",
214+
width / 2, height - 28, CommonColors.LIGHT_GRAY);
215+
context.centeredText(font,
216+
"Multipass/audio/video/cubemap Shadertoys are still unsupported.",
217+
width / 2, height - 16, CommonColors.LIGHT_GRAY);
218+
}
219+
}

src/main/java/net/wurstclient/options/WurstOptionsScreen.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,11 @@ private void addCoreSection()
218218
b -> titleScreenShadertoyBackground
219219
.setChecked(!titleScreenShadertoyBackground.isChecked()));
220220

221+
addButton(column, () -> "Import Shadertoy",
222+
"Import a Shadertoy by URL or pasted source code as the menu background.",
223+
b -> minecraft.gui
224+
.setScreen(new CustomShadertoyBackgroundScreen(this)));
225+
221226
addButton(column,
222227
() -> "Hide Enable Button: " + onOff(hideEnableButton.isChecked()),
223228
hideEnableButton.getDescription(),

src/main/java/net/wurstclient/other_features/WurstOptionsOtf.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import net.wurstclient.settings.ColorSetting;
2424
import net.wurstclient.settings.EnumSetting;
2525
import net.wurstclient.settings.SettingGroup;
26+
import net.wurstclient.settings.TextFieldSetting;
2627
import net.wurstclient.util.text.WText;
2728

2829
@SearchTags({"wurst options", "settings"})
@@ -51,6 +52,12 @@ public final class WurstOptionsOtf extends OtherFeature
5152
"Render the animated Shadertoy-style background on the title screen.",
5253
true);
5354

55+
private final TextFieldSetting titleScreenShadertoyUrl =
56+
new TextFieldSetting("Custom Shadertoy URL",
57+
"Paste a single-pass Shadertoy URL to use as the menu background.",
58+
"", s -> s == null || s.isBlank() || s.matches(
59+
"https?://(www\\.)?shadertoy\\.com/(view|embed)/[A-Za-z0-9]{6}.*"));
60+
5461
private final ColorSetting mojangLogoBackgroundColor =
5562
new ColorSetting("Mojang logo background color", Color.BLACK);
5663

@@ -88,6 +95,7 @@ public void linkAdditionalSettings(DisableOtf disableOtf,
8895
addSetting(customMojangLogoBackground);
8996
addSetting(customMultiplayerLayout);
9097
addSetting(titleScreenShadertoyBackground);
98+
addSetting(titleScreenShadertoyUrl);
9199
addSetting(mojangLogoBackgroundColor);
92100
addSetting(disableOtf.getHideEnableButtonSetting());
93101
addSetting(noTelemetryOtf.getDisableTelemetrySetting());
@@ -137,6 +145,11 @@ public CheckboxSetting getTitleScreenShadertoyBackgroundSetting()
137145
return titleScreenShadertoyBackground;
138146
}
139147

148+
public TextFieldSetting getTitleScreenShadertoyUrlSetting()
149+
{
150+
return titleScreenShadertoyUrl;
151+
}
152+
140153
public boolean isTitleScreenShadertoyBackgroundEnabled()
141154
{
142155
return titleScreenShadertoyBackground.isChecked();

0 commit comments

Comments
 (0)