====== 使用自定义模型动态渲染方块和物品 ====== 可以通过方块模型 JSON 文件将模型添加到游戏,但也可以通过 Java 代码来渲染。本教程中,我们将会把一个四面熔炉模型添加到游戏。 注意模型会在区块被重建时渲染。如果需要更加动态的渲染,可以使用 ''BlockEntityRenderer'':[[zh_cn:tutorial:blockentityrenderers|方块实体渲染器]]。 ===== 创建模型 ===== 模型第一次在 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''。这里,我们会使用两个熔炉纹理,都是方块纹理,所以要从方块atlas''PlayerScreenHandler.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 getModelDependencies() { return Collections.emptyList(); // 模型不依赖于其他模型。 } @Override public void setParents(Function modelLoader) { // 与模型继承有关,我们这里还不需要使用到 } @Override public BakedModel bake(Baker baker, Function 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 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 supplier, RenderContext renderContext) { // 渲染函数 // 我们仅渲染 mesh renderContext.meshConsumer().accept(mesh); } @Override public void emitItemQuads(ItemStack itemStack, Supplier 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" ] }, [...] } ===== 使用模型 ===== 你可以[[blocks|注册方块]]以使用你的新模型。我们假设方块的 id 是 ''tutorial:four_sided_furnace''。 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))); [...] } { "variants": { "": { "model": "tutorial:block/four_sided_furnace" } } } 当然,你可以实现更加复杂的渲染。玩得开心! {{:tutorial:four_sided_furnace_render.png?nolink&600|}} ===== 渲染物品 ===== 正如上图中看到的,物品渲染不正确。我们来将其修复。 ==== 更新模型 ==== 我们复用相同的模型类,但是有一点点小改变: * 我们会使用 ''ModelTransformation'',根据其位置(在右手、在左手、在 GUI 中、在物品展示框中,等等)将其旋转/平移/缩放。就像为一般的方块创建模型一样,我们可以使用由 Fabric 在 ''ModelHelper.MODEL_TRANSFORM_BLOCK'' 提供的变换。 现在对 ''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 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; } }); } } ===== 最终结果 ===== {{:tutorial:four_sided_furnace_render_final.png?nolink&600|}} Et voilà! 享受吧! ===== 更加动态的渲染 ===== ''emitBlockQuads'' 和 ''emitItemQuads'' 中的 ''renderContext'' 参数包含一个你可以用于实时建立模型的 ''QuadEmitter''。 @Override public void emitBlockQuads(BlockRenderView blockRenderView, BlockState blockState, BlockPos blockPos, Supplier supplier, RenderContext renderContext) { QuadEmitter emitter = renderContext.getEmitter(); /* 有了这个emitter,你可以直接将quards添加到区块模型。 */ }