====== 使用自定义模型动态渲染方块和物品 ======
可以通过方块模型 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''SpriteAtlasTexture.BLOCK_ATLAS_TEX''中加载。
private static final SpriteIdentifier[] SPRITE_IDS = new SpriteIdentifier[]{
new SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE, new Identifier("minecraft:block/furnace_front_on")),
new SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE, new Identifier("minecraft:block/furnace_top"))
};
private Sprite[] SPRITES = new Sprite[2];
// Some constants to avoid magic numbers, these need to match the SPRITE_IDS
private static final int SPRITE_SIDE = 0;
private static final int SPRITE_TOP = 1;
==== Meshes ====
''Mesh'' 是准备通过 Fabric Rendering API 渲染的游戏形状。我们会添加一个 Mesh 到我们的类中,然后在 bake 模型时将其构造(build)。
private Mesh mesh;
==== UnbakedModel方法 ====
@Override
public Collection getModelDependencies() {
return Collections.emptyList(); // 模型不依赖于其他模型。
}
@Override
public Collection getTextureDependencies(Function unbakedModelGetter, Set> unresolvedTextureReferences) {
return Arrays.asList(SPRITE_IDS); // 本模型(以及其模型依赖,依赖的依赖,等)依赖的纹理。
}
@Override
public BakedModel bake(ModelLoader loader, Function textureGetter, ModelBakeSettings rotationContainer, Identifier modelId) {
// 获得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
}
@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) {
}
}
===== 注册模型 =====
我们先写一个 ''ModelResourceProvider'',这个接口允许你在游戏尝试从 JSON 加载时提供一个''UnbakedModel''。参看[[https://github.com/FabricMC/fabric/blob/1.16/fabric-models-v0/src/main/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java|本文档]]以了解详情。需要注意的是,对于每个模型,''loadModelResource()'' 都会调用一次。
我们用 ''tutorial:block/four_sided_furnace'' 这个名称注册模型。
@Environment(EnvType.CLIENT)
public class TutorialModelProvider implements ModelResourceProvider {
public static final Identifier FOUR_SIDED_FURNACE_MODEL = new Identifier("tutorial:block/four_sided_furnace");
@Override
public UnbakedModel loadModelResource(Identifier identifier, ModelProviderContext modelProviderContext) throws ModelProviderException {
if(identifier.equals(FOUR_SIDED_FURNACE_MODEL)) {
return new FourSidedFurnaceModel();
} else {
return null;
}
}
}
现在我们要将这个类注册到客户端初始化器(仅适用于客户端的代码的入口点)中。
@Environment(EnvType.CLIENT)
public class ExampleModClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
ModelLoadingRegistry.INSTANCE.registerResourceProvider(rm -> new TutorialModelProvider());
/* 其他客户端指定的初始化 */
}
}
不要忘记在 ''fabric.mod.json'' 中注册这个入口点,如果还没有完成的话:
/* ... */
"entrypoints": {
/* ... */
"client": [
"net.fabricmc.example.ExampleModClient"
]
},
===== 使用模型 =====
你现在可以注册你的方块以使用新模型。比如,如果你的方块只有一个状态,把这个放在''assets/你的模组id/blockstates/你的方块id.json''之下。
{
"variants": {
"": { "model": "tutorial:block/four_sided_furnace" }
}
}
当然,你可以实现更加复杂的渲染。玩得开心!
{{:tutorial:four_sided_furnace_render.png?nolink&600|}}
===== 渲染物品 =====
正如上图中看到的,物品渲染不正确。我们来将其修复。
==== 更新模型 ====
我们复用相同的模型类,但是有一点点小改变:
* 我们会使用 ''ModelTransformation'',根据其位置(在右手、在左手、在 GUI 中、在物品展示框中,等等)将其旋转/平移/缩放。就像为平常的方块创建模型一样,我们将使用会在 bake 模型时加载的“minecraft:block/block”的格式。
现在对 ''FourSidedFurnaceModel'' 类进行如下修改:
// Minecraft 默认方块模型
private static final Identifier DEFAULT_BLOCK_MODEL = new Identifier("minecraft:block/block");
private ModelTransformation transformation;
// 将默认模型添加到依赖中
public Collection getModelDependencies() {
return Arrays.asList(DEFAULT_BLOCK_MODEL);
}
// 我们给 bake 函数添加一点逻辑
@Override
public BakedModel bake(ModelLoader loader, Function textureGetter, ModelBakeSettings rotationContainer, Identifier modelId) {
// 加载默认方块模型
JsonUnbakedModel defaultBlockModel = (JsonUnbakedModel) loader.getOrLoadModel(DEFAULT_BLOCK_MODEL);
// 获取 ModelTransformation
transformation = defaultBlockModel.getTransformations();
/* 先前的代码 */
}
// 我们需要实现 getTransformation() 和 getOverrides()
@Override
public ModelTransformation getTransformation() {
return transformation;
}
@Override
public ModelOverrideList getOverrides() {
return ModelOverrideList.EMPTY;
}
// 我们也会使用此方法以使得物品渲染时有正确的光照。尝试将其设为 false,你就会看到不同。
@Override
public boolean isSideLit() {
return true;
}
// 最终,我们实现物品渲染函数
@Override
public void emitItemQuads(ItemStack itemStack, Supplier supplier, RenderContext renderContext) {
renderContext.meshConsumer().accept(mesh);
}
==== 加载模型 ====
更新我们先前创建的 ''ModelResourceProvider'':
@Environment(EnvType.CLIENT)
public class TutorialModelProvider implements ModelResourceProvider {
public static final FourSidedFurnaceModel FOUR_SIDED_FURNACE_MODEL = new FourSidedFurnaceModel();
public static final Identifier FOUR_SIDED_FURNACE_MODEL_BLOCK = new Identifier("tutorial:block/four_sided_furnace");
public static final Identifier FOUR_SIDED_FURNACE_MODEL_ITEM = new Identifier("tutorial:item/four_sided_furnace");
@Override
public UnbakedModel loadModelResource(Identifier identifier, ModelProviderContext modelProviderContext) throws ModelProviderException {
if(identifier.equals(FOUR_SIDED_FURNACE_MODEL_BLOCK) || identifier.equals(FOUR_SIDED_FURNACE_MODEL_ITEM)) {
return FOUR_SIDED_FURNACE_MODEL;
} else {
return null;
}
}
}
===== 最终结果 =====
{{:tutorial:four_sided_furnace_render_final.png?nolink&600|}}
Et voilà! 享受吧!
===== 更加动态的渲染 =====
''emitBlockQuads'' 和 ''emitItemQuads'' 中的 ''renderContext'' 参数包含一个你可以用于在飞行中(on the fly)建立模型的 ''QuadEmitter''。
@Override
public void emitBlockQuads(BlockRenderView blockRenderView, BlockState blockState, BlockPos blockPos, Supplier supplier, RenderContext renderContext) {
QuadEmitter emitter = renderContext.getEmitter();
/* 有了这个emitter,你可以直接将quards添加到区块模型。 */
}