Skip to content

Vicecorehp/fabric-mod-dev-learning

 
 

Repository files navigation

CustomBackgrounds Mod — Fabric 学习项目

项目概览

一个 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 一致

Mod 元信息:fabric.mod.json

{
  "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_IDLOGGER
CustomBackgroundsClient ClientModInitializer,调用 loadBackground()
BackgroundManager 扫描配置目录、ImageIO 解码、延迟注册 DynamicTexture
TitleScreenMixin TitleScreen.renderPanorama 替换主菜单全景背景(含 fade-in 动画兼容)
ScreenBackgroundMixin Screen.renderBackground HEAD 替换所有未覆盖 renderBackground 的屏幕背景
ReceivingLevelScreenMixin ReceivingLevelScreen.renderBackground HEAD 替换进入世界过渡画面的背景
GenericMessageScreenMixin GenericMessageScreen.renderBackground HEAD 替换"保存世界中"等提示画面的背景

Mixin 配置

mixins.json 中的 package 字段指定扫描路径。Mixin 类通过 @Mixin 注解指定目标类,用 @Inject 在特定位置插入代码。


Fabric 拆分源集机制

src/main/java/   → 编译为 common 源集(服务端 + 通用)
src/client/java/ → 编译为 client 源集(仅客户端)
  • Loom 的 splitEnvironmentSourceSets() 将客户端代码隔离
  • fabric.mod.jsonmain entrypoint 注册通用入口,client entrypoint 注册客户端入口
  • 客户端 Mixin 需在 mixins 中标注 "environment": "client",放在单独的 client.mixins.json

核心研究:TitleScreen 全景渲染流程

渲染调用链

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_MAPPANORAMA
GuiGraphics net.minecraft.client.gui 绘制工具类,封装 RenderSystem + Tesselator

原版全景渲染细节

  1. TitleScreen.renderPanorama() 调用 PANORAMA.render()
  2. PanoramaRenderer 更新旋转角度(spin += delta * speed * 0.1)和偏移(bob
  3. CubeMap.render() 进行 4 次 Pass(抖动透明叠加),每次绘制 6 个面
  4. 每个面绑定对应纹理:textures/gui/title/background/panorama_{0-5}.png
  5. 最后叠加渐暗叠层 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

关键 API

关键方法 说明
NativeImage read(InputStream) 仅支持 PNG
DynamicTexture DynamicTexture(NativeImage) 包装 NativeImage → OpenGL 纹理(构造时调 glGenTextures
TextureManager register(ResourceLocation, AbstractTexture) 注册纹理供后续使用
ImageIO read(File) 标准 Java 图片解码(JPEG/PNG/BMP 等)

CustomBackgrounds Mod 实现

功能

用户将 .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() 仅解码图片存为 NativeImageDynamicTexture 等到 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 覆盖的纯净背景。

Cover 裁剪算法

屏幕宽高比 > 图片宽高比   → 缩放至宽度匹配,裁剪上下
屏幕宽高比 < 图片宽高比   → 缩放至高度匹配,裁剪左右
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/ 文件夹。


License

This template is available under the CC0 license. Feel free to learn from it and incorporate it in your own projects.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • Java 100.0%