Table of Contents

使用自定义模型动态渲染方块和物品

可以通过方块模型 JSON 文件将模型添加到游戏,但也可以通过 Java 代码来渲染。本教程中,我们将会把一个四面熔炉模型添加到游戏。

注意模型会在区块被重建时渲染。如果需要更加动态的渲染,可以使用 BlockEntityRenderer方块实体渲染器

创建模型

模型第一次在 Minecraft 注册时,其原始数据被包含在 UnbakedModel 中。这个数据可能会包括形状(shapes)或纹理名称(texture name)。然后在初始化过程中,UnbakedModel::bake() 创建一个BakedModel 以准备渲染。为了使得渲染尽可能快,bake 的过程中需要完成尽可能多的操作。我们也会实现 FabricBakedModel 以充分利用 Fabric Renderer API。现在创建一个实现所有三个接口的单个 FourSidedFurnace 模型。

@Environment(EnvType.CLIENT)
public class FourSidedFurnaceModel implements UnbakedModel, BakedModel, FabricBakedModel {

Sprites

渲染纹理离不开Sprite。我们必须先创建一个SpriteIdentifier然后在bake模型时得到对应的Sprite。这里,我们会使用两个熔炉纹理,都是方块纹理,所以要从方块atlasPlayerScreenHandler.BLOCK_ATLAS_TEXTURE中加载。

    // 对于 1.21 之前的版本,将 `Identifier.ofVanilla` 替换为 `new Identifier`。
    private static final SpriteIdentifier[] SPRITE_IDS = new SpriteIdentifier[]{
            new SpriteIdentifier(PlayerScreenHandler.BLOCK_ATLAS_TEXTURE, Identifier.ofVanilla("block/furnace_front_on")),
            new SpriteIdentifier(PlayerScreenHandler.BLOCK_ATLAS_TEXTURE, Identifier.ofVanilla("block/furnace_top"))
    };
    private Sprite[] SPRITES = new Sprite[SPRITE_IDS.length];
 
    // 一些常量,以避免魔法数据,需要匹配 SPRITE_IDS
    private static final int SPRITE_SIDE = 0;
    private static final int SPRITE_TOP = 1;

Mesh

Mesh 是准备通过 Fabric Rendering API 渲染的游戏形状。我们会添加一个 Mesh 到我们的类中,然后在 bake 模型时将其构造(build)。

    private Mesh mesh;

UnbakedModel 方法

    @Override
    public Collection<Identifier> getModelDependencies() {
        return Collections.emptyList(); // 模型不依赖于其他模型。
    }
 
    @Override
    public void setParents(Function<Identifier, UnbakedModel> modelLoader) {
        // 与模型继承有关,我们这里还不需要使用到
    }
 
 
    @Override
    public BakedModel bake(Baker baker, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings rotationContainer) {
        // 获得 sprites
        for(int i = 0; i < 2; ++i) {
            SPRITES[i] = textureGetter.apply(SPRITE_IDS[i]);
        }
        // 用 Renderer API 构建 mesh
        Renderer renderer = RendererAccess.INSTANCE.getRenderer();
        MeshBuilder builder = renderer.meshBuilder();
        QuadEmitter emitter = builder.getEmitter();
 
        for(Direction direction : Direction.values()) {
            int spriteIdx = direction == Direction.UP || direction == Direction.DOWN ? SPRITE_TOP : SPRITE_SIDE;
            // 将新的面(face)添加到 mesh
            emitter.square(direction, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f);
            // 设置面的 sprite,必须在 .square() 之后调用
            // 我们还没有指定任何 uv 坐标,所以我们使用整个纹理,BAKE_LOCK_UV 恰好就这么做。
            emitter.spriteBake(0, SPRITES[spriteIdx], MutableQuadView.BAKE_LOCK_UV);
            // 启用纹理使用
            emitter.spriteColor(0, -1, -1, -1, -1);
            // 将 quad 添加到 mesh
            emitter.emit();
        }
        mesh = builder.build();
 
        return this;
    }

BakedModel 方法

注意这里不是所有的方法都会被 Fabric Renderer 使用,所以我们并不关心这个实现。

    @Override
    public List<BakedQuad> getQuads(BlockState state, Direction face, Random random) {
        return Collections.emptyList(); // 不需要,因为我们使用的是 FabricBakedModel,但是最好不要返回 null,因为有些模组会要调用这个函数
    }
 
    @Override
    public boolean useAmbientOcclusion() {
        return true; // 环境光遮蔽:我们希望方块在有临近方块时显示阴影
    }
 
    @Override
    public boolean isBuiltin() {
        return false;
    }
 
    @Override
    public boolean hasDepth() {
        return false;
    }
 
    @Override
    public boolean isSideLit() {
        return false;
    }
 
    @Override
    public Sprite getParticleSprite() {
        return SPRITES[1]; // 方块被破坏时产生的颗粒,使用 furnace_top
    }
 
    @Override
    public ModelTransformation getTransformation() {
        return null;
    }
 
    @Override
    public ModelOverrideList getOverrides() {
        return null;
    }

FabricBakedModel 方法

    @Override
    public boolean isVanillaAdapter() {
        return false; // false 以触发 FabricBakedModel 渲染
    }
 
    @Override
    public void emitBlockQuads(BlockRenderView blockRenderView, BlockState blockState, BlockPos blockPos, Supplier<Random> supplier, RenderContext renderContext) {
        // 渲染函数
 
        // 我们仅渲染 mesh
        renderContext.meshConsumer().accept(mesh);
    }
 
    @Override
    public void emitItemQuads(ItemStack itemStack, Supplier<Random> supplier, RenderContext renderContext) {
 
    }
}

注意:确保覆盖了 FabricBakedModel 方法,接口有 default 的实现!

注册模型

要让模型在游戏内被渲染需要注册,为注册我们需要创建 ModelLoadingPlugin

@Environment(EnvType.CLIENT)
public class TutorialModelLoadingPlugin implements ModelLoadingPlugin {
    public static final ModelIdentifier FOUR_SIDED_FURNACE_MODEL = new ModelIdentifier(Identifier.of("tutorial", "four_sided_furnace"), "");
 
    @Override
    public void onInitializeModelLoader(Context pluginContext) {
        // 我们需要在模型被加载时添加模型
        pluginContext.modifyModelOnLoad().register((original, context) -> {
            // 这个每次加载模型时都会调用,所以确保我们只针对我们的
            final ModelIdentifier id = context.topLevelId();
            if(id != null && id.equals(FOUR_SIDED_FURNACE_MODEL)) {
                return new FourSidedFurnaceModel();
            } else {
                // 如果不修改模型,就照样返回原来的
                return original;
            }
        });
    }
}

现在我们要将这个类注册到客户端初始化器(仅适用于客户端的代码的入口点)中。

@Environment(EnvType.CLIENT)
public class ExampleModClient implements ClientModInitializer {
    @Override
    public void onInitializeClient() {
        ModelLoadingPlugin.register(new TutorialModelLoadingPlugin());
 
        /* 其他客户端特定的初始化 */
    }
}

不要忘记在 fabric.mod.json 中注册这个入口点,如果还没有完成的话:

{
  [...]
  "entrypoints": {
    [...]
    "client": [
      "net.fabricmc.example.ExampleModClient"
    ]
  },
  [...]
}

使用模型

你可以注册方块以使用你的新模型。我们假设方块的 id 是 tutorial:four_sided_furnace

TutorialBlocks.java
public final class TutorialBlocks {
  [...]
  public static final Block FOUR_SIDED_FURNACE = register("four_sided_furnace", new Block(AbstractBlock.Settings.copy(Blocks.FURNACE).luminance(x -> 15)));
  [...]
}
src/main/resources/assets/tutorial/blockstates/four_sided_furnace.json
{
  "variants": {
    "": { "model": "tutorial:block/four_sided_furnace" }
  }
}

当然,你可以实现更加复杂的渲染。玩得开心!

渲染物品

正如上图中看到的,物品渲染不正确。我们来将其修复。

更新模型

我们复用相同的模型类,但是有一点点小改变:

现在对 FourSidedFurnaceModel 类进行如下修改:

    // 我们需要实现 getTransformation() 和 getOverrides()
    @Override
    public ModelTransformation getTransformation() {
        return ModelHelper.MODEL_TRANSFORM_BLOCK;
    }
 
    @Override
    public ModelOverrideList getOverrides() {
        return ModelOverrideList.EMPTY;
    }
 
    // 我们也会使用此方法以使得物品渲染时有正确的光照。尝试将其设为 false,你就会看到不同。
    @Override
    public boolean isSideLit() {
        return true;
    }
 
    // 最终,我们实现物品渲染函数
    @Override
    public void emitItemQuads(ItemStack itemStack, Supplier<Random> supplier, RenderContext renderContext) {
        mesh.outputTo(context.getEmitter());
    }

加载模型

更新我们先前创建的 TutorialModelLoadingPlugin

@Environment(EnvType.CLIENT)
public class TutorialModelLoadingPlugin implements ModelLoadingPlugin {
    public static final ModelIdentifier FOUR_SIDED_FURNACE_MODEL = new ModelIdentifier(Identifier.of("tutorial", "four_sided_furnace"), "");
    public static final ModelIdentifier FOUR_SIDED_FURNACE_MODEL_ITEM = new ModelIdentifier(Identifier.of("tutorial", "four_sided_furnace"), "inventory");
 
    @Override
    public void onInitializeModelLoader(Context pluginContext) {
        // 我们需要在模型被加载时添加模型
        pluginContext.modifyModelOnLoad().register((original, context) -> {
            // 这个每次加载模型时都会调用,所以确保我们只针对我们的
            final ModelIdentifier id = context.topLevelId();
            if (id != null && (id.equals(FOUR_SIDED_FURNACE_MODEL) || id.equals(FOUR_SIDED_FURNACE_MODEL_ITEM))) {
                return new FourSidedFurnaceModel();
            } else {
                // 如果不修改模型,就照样返回原来的
                return original;
            }
        });
    }
}

最终结果

Et voilà! 享受吧!

更加动态的渲染

emitBlockQuadsemitItemQuads 中的 renderContext 参数包含一个你可以用于实时建立模型的 QuadEmitter

    @Override
    public void emitBlockQuads(BlockRenderView blockRenderView, BlockState blockState, BlockPos blockPos, Supplier<Random> supplier, RenderContext renderContext) {
        QuadEmitter emitter = renderContext.getEmitter();
        /* 有了这个emitter,你可以直接将quards添加到区块模型。 */
    }