🎬 CraftEngine 动画图片系统

一、简介

CraftEngineAnim —— 一款 Minecraft 着色器帧动画系统

CraftEngineAnim 通过精灵图(Sprite Sheet)与自定义着色器,实现在 Minecraft 聊天、GUI、物品名称等任何支持 bitmap font 的位置播放帧动画。基于 GameTime 驱动自动切帧,无需服务端参与,完全客户端渲染。

CraftEngineAnim 支持 Minecraft 1.21.4+ 版本,配合 CraftEngine 资源包系统使用,并且会第一时间支持未来版本。

CraftEngineAnim

  • 着色器驱动 > 顶点着色器(vsh)拦截标记颜色,自动计算帧号并重映射UV
  • 精灵图 > 多帧合并为单张精灵图,支持任意网格尺寸
  • Bitmap Font > 通过 CraftEngine 注册为自定义字符使用
  • 两种模式 > Legacy模式(4帧固定) + Metadata模式(无限帧)
  • GIF转换 > 附带 Python 工具自动将 GIF 转换为精灵图
  • 表情符号 > 可用作聊天表情、物品描述装饰
  • 零服务端开销 > 纯客户端渲染,不影响服务器性能
  • 可视化编辑器 > 内置精灵图编辑器,实时预览

二、插件前置说明

都是非必须(除 CraftEngine)

  • CraftEngine — 必需,资源包管理框架
💡 核心思路

多帧动画 → 精灵图(Sprite Sheet) → bitmap font 字符 → 着色器按 GameTime 自动切帧

渲染管线

玩家输入 :allay: ↓ CraftEngine → <!shadow><color:#fd0900><image:minecraft:allay_gif> ↓ Minecraft 渲染文字 → 顶点颜色 = #fd0900 (R=253, G=9, B=0) ↓ 顶点着色器 (vsh) 拦截标记颜色 → 读取精灵图(0,0)元数据 → 计算帧号 → 重映射 UV ↓ 片段着色器 (fsh): 100% 原版 → 采样重映射 UV → 显示单帧

标记颜色

R
253 固定
+
G
1~50 动画ID
+
B
0 校验
=
🎬
#fdXX00

UV 重映射

4×4 网格精灵图 (256×256, 每帧 64×64): ┌──────┬──────┬──────┬──────┐ │ F0F1F2F3 │ ├──────┼──────┼──────┼──────┤ │ F4F5F6F7 │ ├──────┼──────┼──────┼──────┤ │ F8F9F10F11 │ ├──────┼──────┼──────┼──────┤ │ F12F13F14F15 │ └──────┴──────┴──────┴──────┘ 当前帧=F5: col=5%4=1, row=5/4=1 → UV 映射到第1列第1行

目录结构

animated_text/ ├── pack.yml CraftEngine 包配置 ├── wiki.html 本文档 ├── configuration/ │ └── animated_images.yml 图片+emoji+模板配置 ├── resourcepack/assets/minecraft/ │ ├── shaders/core/ │ │ ├── rendertype_text.vsh 顶点着色器(核心) │ │ ├── rendertype_text.fsh 片段着色器(原版) │ │ └── rendertype_text.jsonuniform定义 │ └── textures/font/anim/ │ ├── *_sprite.png Legacy精灵图 │ └── *_gif.png Metadata精灵图 └── tools/ ├── gen_test_sprites.py 测试生成器 └── gif_to_sprite.py GIF转换工具

🎛️ 动画模式

Legacy 模式 G=1~5

属性
精灵图256×256, 2×2 网格, 4 帧固定
元数据不需要
速度G 值编码 fps
G颜色fps动画
1#fd01001.5fire_anim
2#fd02001.0water_anim
3#fd03002.0lightning_anim
4#fd04000.8heartbeat_anim
5#fd05001.2rainbow_anim

Pixel-metadata 模式 G=6~50 推荐

精灵图左上角像素(0,0)编码动画参数,支持任意网格。

R
速度倍率
1~255
G
帧宽(px)
如64
B
帧高(px)
如64
A
速度档
1/2/3
通道含义说明
R速度倍率值越大越快,推荐6~12
G帧宽如64=每帧宽64px
B帧高如64=每帧高64px
A速度档3=慢(×1000) 2=中(×500) 1=快(×300)
⚠️ Alpha 必须是 1/2/3

着色器用此验证元数据有效性。其他值=无效。

案例: allay_gif.png

像素(0,0) = RGBA(8, 64, 64, 3)
  → speed=8, frameW=64, frameH=64, tier=3(慢)
  → 网格: 256/64=4列, 256/64=4行 = 16帧
  → 实际速度: GameTime × 1000 × 8

📖 制作教程

教程 A: 从 GIF 制作精灵图

方法 1: 转换工具 (推荐)

python tools/gif_to_sprite.py npc_walk.gif npc_walk_gif.png
python tools/gif_to_sprite.py npc_walk.gif npc_walk_gif.png --speed 12 --tier 2
python tools/gif_to_sprite.py --verify allay_gif.png

方法 2: 手动 Python

from PIL import Image

gif = Image.open("my_animation.gif")
frames = []
try:
    while True:
        frames.append(gif.copy().convert("RGBA"))
        gif.seek(gif.tell() + 1)
except EOFError: pass

FRAME_SIZE = 64
resized = [f.resize((FRAME_SIZE, FRAME_SIZE), Image.LANCZOS) for f in frames]

sprite = Image.new("RGBA", (256, 256), (0, 0, 0, 0))
for i in range(16):
    frame = resized[i % len(resized)]
    col, row = i % 4, i // 4
    sprite.paste(frame, (col * FRAME_SIZE, row * FRAME_SIZE))

# 嵌入元数据: R=速度, G=帧宽, B=帧高, A=档位
sprite.putpixel((0, 0), (8, 64, 64, 3))
sprite.save("my_animation_gif.png")
⚠️ 重要

精灵图必须 256×256 正方形! 非正方形会导致渲染变形。

教程 B: 添加新动画 (wizard, G=20)

# Step 1: G=20 → hex(20)=14 → 颜色 #fd1400
# Step 2: 制作精灵图
python tools/gif_to_sprite.py wizard.gif wizard_gif.png --speed 8 --tier 3
# Step 3: 放到 textures/font/anim/wizard_gif.png
# Step 4: animated_images.yml
images:
  minecraft:wizard_gif:
    height: 48
    ascent: 42
    font: minecraft:default
    file: minecraft:font/anim/wizard_gif.png

templates:
  minecraft:anim_emoji/wizard:
    content: <!shadow><color:#fd1400><image:minecraft:wizard_gif></color></!shadow>

emoji:
  minecraft:gif_wizard:
    content: <!shadow><color:#fd1400><image:minecraft:wizard_gif></color></!shadow>
    image: minecraft:wizard_gif
    permission: emoji.gif.wizard
    keywords:
      - ':wizard:'
💡 无需改着色器

Pixel-metadata 模式只需分配 G 值 + 放精灵图 + 写配置。

💻 着色器详解

顶点着色器 rendertype_text.vsh

标记检测
元数据模式
Legacy模式
颜色处理
ivec3 iColor = ivec3(Color.rgb * 255.0 + 0.5);

if (iColor.r == 253 && iColor.b == 0 && iColor.g >= 1 && iColor.g <= 50) {
    ivec2 atlasSize = textureSize(Sampler0, 0);
    float aw = float(atlasSize.x);
    float ah = float(atlasSize.y);
    int vid = gl_VertexID % 4;
    bool isRight  = (vid >= 2);
    bool isBottom = (vid == 1 || vid == 2);
}
// Pixel-metadata (G >= 6)
float tryW = 256.0;
float cw = tryW / aw, ch = 256.0 / ah;
float uMin = UV0.x - (isRight ? cw : 0.0);
float vMin = UV0.y - (isBottom ? ch : 0.0);
ivec2 tlPx = ivec2(uMin * aw + 0.5, vMin * ah + 0.5);
vec4 meta = texelFetch(Sampler0, tlPx, 0) * 255.0;

float frameW = floor(meta.g + 0.5);
float frameH = floor(meta.b + 0.5);
float speed  = floor(meta.r + 0.5);
float speedMul = 1000.0; // A=3→1000, A=2→500, A=1→300

int gridCols = int(charPixW / frameW);
int totalFrames = gridCols * int(256.0 / frameH);
int frame = int(mod(GameTime * speedMul * speed, float(totalFrames)));

float fcol = float(frame % gridCols);
float frow = float(frame / gridCols);
texCoord0 = vec2(
    uMin + (fcol + (isRight ? 1.0 : 0.0)) * frameW/aw,
    vMin + (frow + (isBottom ? 1.0 : 0.0)) * frameH/ah
);
// Legacy (G=1..5) 固定 2×2, 4帧
float fps = 1.0;
if (iColor.g == 1) fps = 1.5;
if (iColor.g == 2) fps = 1.0;
if (iColor.g == 3) fps = 2.0;
if (iColor.g == 4) fps = 0.8;
if (iColor.g == 5) fps = 1.2;

int frame = int(mod(GameTime * 1200.0 * fps, 4.0));
float col = mod(float(frame), 2.0);
float row = floor(float(frame) / 2.0);
// 动画字符: 不乘 Color (标记色会变红)
vertexColor = texelFetch(Sampler2, UV2 / 16, 0);

// 非动画字符: 原版
vertexColor = Color * texelFetch(Sampler2, UV2 / 16, 0);
texCoord0 = UV0;

片段着色器 rendertype_text.fsh

🚫 绝对不修改!

添加自定义 varying 会导致 1.21.4 所有文字乱码!

// 100% 原版
vec4 color = texture(Sampler0, texCoord0) * vertexColor * ColorModulator;
if (color.a < 0.1) discard;
fragColor = linear_fog(color, vertexDistance, FogStart, FogEnd, FogColor);

rendertype_text.json

// uniforms 必须包含 GameTime
{ "name": "GameTime", "type": "float", "count": 1, "values": [ 0.0 ] }

⚙️ animated_images.yml

# 1. images: 注册图片
images:
  minecraft:allay_gif:
    height: 48
    ascent: 42
    font: minecraft:default
    file: minecraft:font/anim/allay_gif.png

# 2. templates: MiniMessage 模板
templates:
  minecraft:anim_emoji/allay:
    content: <!shadow><color:#fd0900><image:minecraft:allay_gif></color></!shadow>

# 3. emoji: 聊天触发
emoji:
  minecraft:gif_allay:
    content: <!shadow><color:#fd0900><image:minecraft:allay_gif></color></!shadow>
    image: minecraft:allay_gif
    permission: emoji.gif.allay
    keywords:
      - ':allay:'
⚠️ <!shadow> 必须加!

阴影使 Color×0.25, R从253变63, 着色器无法识别→显示整张精灵图。

🛠️ 精灵图编辑器

两种方式创建动画精灵图,自动生成配置文件并打包成 ZIP 下载。

📁 方式一: 多张帧图片
🖼 方式二: 从整图选取帧

上传帧图片

📂
拖拽 GIF 或多张 PNG/JPG 帧图到此处
GIF 自动拆帧 · 点击选择文件

帧排序 (拖拽排序 · 点击选中 · ×删除)

上传整图

🖼
拖拽一张包含多帧的精灵图/合图到此处
PNG / JPG · 点击选择文件

配置参数

生成与下载

📋 速查与 FAQ

颜色编码速查表

G颜色动画模式关键词
1#fd0100fire_animLegacy:fire:
2#fd0200water_animLegacy:water:
3#fd0300lightning_animLegacy:lightning:
4#fd0400heartbeat_animLegacy:heartbeat:
5#fd0500rainbow_animLegacy:rainbow:
6~19#fd0600~#fd1300各 Meta 动画Meta
20~50✅ 31 个空位可用
# G → 颜色代码: #fd + hex(G, 2位) + 00
G=20 → #fd1400   G=30 → #fd1e00   G=50 → #fd3200

速度参考

Speed(R)Tier=3(慢)Tier=2(中)Tier=1(快)
4很慢
6较快
8推荐较快
12较快很快

常见问题

Q: 显示整张精灵图(不动画)

检查 <!shadow> 是否添加, 颜色代码是否正确, G值是否在1~50

Q: 所有文字乱码

fsh被修改了! 确保 rendertype_text.fsh 是100%原版

Q: 动画显示窄长

精灵图不是正方形. 重排帧到256×256画布

Q: Metadata模式不播放

像素(0,0) Alpha 不是 1/2/3. 验证: img.getpixel((0,0))[3]