一个 Fabric 1.21 Mod 开发学习项目,目标实现主菜单背景自定义功能:用户将图片放入指定文件夹,游戏主菜单背景自动替换为自定义图片。
| 技术 | 版本 |
|---|---|
| Minecraft | 1.21 |
| Fabric Loader | 0.19.3 |
| Fabric API | 0.102.0+1.21 |
| Java | 21 |
| 映射表 | Official Mojang Mappings |
fabric-mod-dev-learning/
├── build.gradle # Loom 构建配置
├── gradle.properties # 版本号管理
├── settings.gradle # 项目名 custombackgrounds
├── gradlew / gradlew.bat # Gradle Wrapper
│
└── src/
├── main/ # 通用 + 服务端代码
│ ├── java/com/example/custombackgrounds/
│ │ └── CustomBackgrounds.java # ModInitializer 主入口(仅 MOD_ID + LOGGER)
│ └── resources/
│ └── fabric.mod.json # Mod 元信息
│
└── client/ # 仅客户端代码
├── java/com/example/custombackgrounds/client/
│ ├── CustomBackgroundsClient.java # ClientModInitializer 入口
│ ├── background/
│ │ └── BackgroundManager.java # 图片扫描、ImageIO 解码、延迟纹理注册
│ └── mixin/
│ ├── TitleScreenMixin.java # TitleScreen.renderPanorama 拦截
│ ├── ScreenBackgroundMixin.java # Screen.renderBackground HEAD 拦截
│ ├── ReceivingLevelScreenMixin.java # ReceivingLevelScreen 单独处理
│ └── GenericMessageScreenMixin.java # GenericMessageScreen 单独处理
└── resources/
└── custombackgrounds.client.mixins.json
| 文件 | 说明 |
|---|---|
build.gradle |
Loom 插件、splitEnvironmentSourceSets() 拆分源集、依赖声明(MC、Loader、Fabric API)、Java 21 编译目标 |
gradle.properties |
统一管理各依赖版本号、Gradle JVM 参数 |
settings.gradle |
rootProject.name = 'custombackgrounds',应与 fabric.mod.json 中的 id 一致 |
{
"id": "custombackgrounds", // Mod 唯一标识
"version": "${version}", // 从 gradle.properties 注入
"entrypoints": {
"main": ["com.example.custombackgrounds.CustomBackgrounds"],
"client": ["com.example.custombackgrounds.client.CustomBackgroundsClient"]
},
"mixins": [
{ "config": "custombackgrounds.client.mixins.json", "environment": "client" }
],
"depends": {
"fabricloader": ">=0.19.3",
"minecraft": "~1.21",
"java": ">=21",
"fabric-api": "*"
}
}| 类 | 目标 | 作用 |
|---|---|---|
CustomBackgrounds |
— | ModInitializer,定义 MOD_ID 与 LOGGER |
CustomBackgroundsClient |
— | ClientModInitializer,调用 loadBackground() |
BackgroundManager |
— | 扫描配置目录、ImageIO 解码、延迟注册 DynamicTexture |
TitleScreenMixin |
TitleScreen.renderPanorama |
替换主菜单全景背景(含 fade-in 动画兼容) |
ScreenBackgroundMixin |
Screen.renderBackground HEAD |
替换所有未覆盖 renderBackground 的屏幕背景 |
ReceivingLevelScreenMixin |
ReceivingLevelScreen.renderBackground HEAD |
替换进入世界过渡画面的背景 |
GenericMessageScreenMixin |
GenericMessageScreen.renderBackground HEAD |
替换"保存世界中"等提示画面的背景 |
mixins.json 中的 package 字段指定扫描路径。Mixin 类通过 @Mixin 注解指定目标类,用 @Inject 在特定位置插入代码。
src/main/java/ → 编译为 common 源集(服务端 + 通用)
src/client/java/ → 编译为 client 源集(仅客户端)
- Loom 的
splitEnvironmentSourceSets()将客户端代码隔离 fabric.mod.json中mainentrypoint 注册通用入口,cliententrypoint 注册客户端入口- 客户端 Mixin 需在
mixins中标注"environment": "client",放在单独的client.mixins.json中
Minecraft.run()
→ TitleScreen.render(guiGraphics, mouseX, mouseY, delta)
→ super.renderBackground(...)
→ if (minecraft.level == null) {
this.renderPanorama(guiGraphics, delta) ← Mixin 拦截点
}
→ renderBlurredBackground(delta)
→ renderMenuBackground(guiGraphics)
→ [渲染子组件:按钮、标题、标语等]
| 类 | 包 | 作用 |
|---|---|---|
TitleScreen |
net.minecraft.client.gui.screens |
主菜单屏幕;重写 renderPanorama() |
PanoramaRenderer |
net.minecraft.client.renderer |
编排全景渲染 + 叠加渐暗叠层 |
CubeMap |
net.minecraft.client.renderer |
渲染 6 面立方体贴图(天空盒风格) |
Screen |
net.minecraft.client.gui.screens |
父类,持有静态 CUBE_MAP 和 PANORAMA |
GuiGraphics |
net.minecraft.client.gui |
绘制工具类,封装 RenderSystem + Tesselator |
TitleScreen.renderPanorama()调用PANORAMA.render()PanoramaRenderer更新旋转角度(spin += delta * speed * 0.1)和偏移(bob)CubeMap.render()进行 4 次 Pass(抖动透明叠加),每次绘制 6 个面- 每个面绑定对应纹理:
textures/gui/title/background/panorama_{0-5}.png - 最后叠加渐暗叠层
panorama_overlay.png
// 1. 读取文件(仅 PNG)
NativeImage image;
try (InputStream is = Files.newInputStream(imagePath)) {
image = NativeImage.read(is);
}注意: Minecraft 1.21 的 NativeImage.read(InputStream) 仅支持 PNG。
JPEG 需通过 javax.imageio.ImageIO 读取 BufferedImage 后再逐像素写入 NativeImage。
| 类 | 关键方法 | 说明 |
|---|---|---|
NativeImage |
read(InputStream) |
仅支持 PNG |
DynamicTexture |
DynamicTexture(NativeImage) |
包装 NativeImage → OpenGL 纹理(构造时调 glGenTextures) |
TextureManager |
register(ResourceLocation, AbstractTexture) |
注册纹理供后续使用 |
ImageIO |
read(File) |
标准 Java 图片解码(JPEG/PNG/BMP 等) |
用户将 .png / .jpg 图片放入 run/config/custombackgrounds/ 目录(或 .minecraft/config/custombackgrounds/),游戏主菜单背景自动替换为该图片,并按 Cover 模式(保持比例裁剪填充)显示。
src/
├── main/java/com/example/custombackgrounds/
│ └── CustomBackgrounds.java
│
└── client/java/com/example/custombackgrounds/client/
├── CustomBackgroundsClient.java
├── background/
│ └── BackgroundManager.java
└── mixin/
├── TitleScreenMixin.java
├── ScreenBackgroundMixin.java
├── ReceivingLevelScreenMixin.java
└── GenericMessageScreenMixin.java
BackgroundManager.java
- 扫描
config/custombackgrounds/下第一个.png/.jpg/.jpeg文件(按文件名排序) - PNG 使用
NativeImage.read(InputStream)直接解码 - JPEG 使用
ImageIO.read(File)→BufferedImage→ 逐像素转 RGBA 写入NativeImage - 延迟创建纹理:
loadBackground()仅解码图片存为NativeImage,DynamicTexture等到getTextureLocation()首次被调用时才创建(避免在 OpenGL 初始化前调glGenTextures()导致崩溃)
Mixin 覆盖策略
| Mixin | 注入目标 | 拦截方式 | 负责的屏幕 |
|---|---|---|---|
TitleScreenMixin |
TitleScreen.renderPanorama() |
@Inject HEAD, cancellable |
主菜单(含 fade-in) |
ScreenBackgroundMixin |
Screen.renderBackground() |
@Inject HEAD, cancellable |
所有未覆盖 renderBackground 的屏幕(世界选择、选项、创建世界、LevelLoadingScreen 等) |
ReceivingLevelScreenMixin |
ReceivingLevelScreen.renderBackground() |
@Inject HEAD, cancellable |
进入世界过渡(下界/末地传送门背景) |
GenericMessageScreenMixin |
GenericMessageScreen.renderBackground() |
@Inject HEAD, cancellable |
"保存世界中""加载中"等提示画面 |
所有 Mixin 都 取消原版的 renderMenuBackground(dirt 纹理叠层),直接绘制自定义图片,实现无 dirt 覆盖的纯净背景。
屏幕宽高比 > 图片宽高比 → 缩放至宽度匹配,裁剪上下
屏幕宽高比 < 图片宽高比 → 缩放至高度匹配,裁剪左右
float screenRatio = (float) screenWidth / screenHeight;
float imageRatio = (float) imgWidth / imgHeight;
if (screenRatio > imageRatio) {
int srcH = imgWidth * screenHeight / screenWidth;
int srcY = (imgHeight - srcH) / 2;
guiGraphics.blit(rl, 0, 0, screenWidth, screenHeight,
0, srcY, imgWidth, srcH, imgWidth, imgHeight);
} else {
int srcW = imgHeight * screenWidth / screenHeight;
int srcX = (imgWidth - srcW) / 2;
guiGraphics.blit(rl, 0, 0, screenWidth, screenHeight,
srcX, 0, srcW, imgHeight, imgWidth, imgHeight);
}| 问题 | 原因 | 解决 |
|---|---|---|
Bad PNG Signature |
NativeImage.read(InputStream) 仅支持 PNG |
JPEG 改用 ImageIO → 逐像素写入 NativeImage |
OpenGL 崩溃(EXCEPTION_ACCESS_VIOLATION) |
DynamicTexture 构造时调 glGenTextures(),但 onInitializeClient() 执行时 OpenGL 上下文未就绪 |
延迟纹理创建到 TitleScreen 首次渲染时 |
| 图片泛红 | ARGB → RGBA 字节序转换错误 | setPixelRGBA(x, y, (a << 24) | (b << 16) | (g << 8) | r) |
| 图片被拉伸 | Cover 裁剪公式算反(混淆了源裁剪量与缩放后尺寸) | srcH = imgWidth * screenH / screenW(而非 imgH * screenW / imgW) |
| 世界选择/加载界面背景未更换 | @At("INVOKE") 无法覆盖 renderBackground 子类重写;@Shadow 无法映射继承方法 |
改用 @At("HEAD") + 独立 Mixin 逐个覆盖,通过 (Screen)(Object)this 访问继承字段 |
| dirt 方块纹理覆盖背景 | renderMenuBackground() 在所有屏幕上都叠加 dirt 半透明叠层 |
所有 Mixin 均 ci.cancel() 跳过原版背景绘制流程,不再调用 renderMenuBackground |
# 构建 Mod
./gradlew build
# 运行 Minecraft(自动加载 Mod)
./gradlew runClient
# 生成 Minecraft 源码(供 IDE 跳转使用)
./gradlew genSources
# IntelliJ IDEA 生成运行配置
./gradlew idea构建产物位于 build/libs/,可直接放入 Minecraft 的 mods/ 文件夹。
This template is available under the CC0 license. Feel free to learn from it and incorporate it in your own projects.