This is an old revision of the document!
Table of Contents
Creating a custom block model (DRAFT)
It is possible to add models to the game using block model JSON files, but it is also possible to render them through Java code. In this tutorial, we will add a four-sided furnace model to the game.
Creating the model
When a model is first registered in Minecraft, its raw data is contained in an UnbakedModel
. This data can include shapes or texture names for example.
Later during the initialization, UnbakedModel::bake()
creates a BakedModel
, ready for rendering. For rendering to be as fast as possible, as many operations as possible need to be done during baking. We will also implement FabricBakedModel
to make use of the Fabric Renderer API.
Let's create a single FourSidedFurnace
model that will implement all three interfaces.
public class FourSidedFurnaceModel implements UnbakedModel, BakedModel, FabricBakedModel {
Sprites
A Sprite
is necessary for rendering a texture. We must first create a SpriteIdentifier
and then get the corresponding Sprite
while baking the model.
Here, we will use two furnace textures. They are block textures, so they must be loaded from the block atlas SpriteAtlasTexture.BLOCK_ATLAS_TEX
.
private static final SpriteIdentifier[] SPRITE_IDS = new SpriteIdentifier[]{ new SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEX, new Identifier("minecraft:block/furnace_front_on")), new SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEX, new Identifier("minecraft:block/furnace_top")) }; private Sprite[] SPRITES = new Sprite[2];
Meshes
A Mesh
is a game shape that is ready for rendering with the Fabric Rendering API. We will add one to our class, and we will build it during model baking.
private Mesh mesh;
''UnbakedModel'' methods
@Override public Collection<Identifier> getModelDependencies() { return Collections.emptyList(); // This model does not depend on other models. } @Override public Collection<SpriteIdentifier> getTextureDependencies(Function<Identifier, UnbakedModel> unbakedModelGetter, Set<Pair<String, String>> unresolvedTextureReferences) { return Arrays.asList(SPRITE_IDS); // The textures this model depends on. } @Override public BakedModel bake(ModelLoader loader, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings rotationContainer, Identifier modelId) { // Get the sprites for(int i = 0; i < 2; ++i) { SPRITES[i] = textureGetter.apply(SPRITE_IDS[i]); } // Build the 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 ? 1 : 0; // Add a new face to the mesh emitter.square(direction, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f); // Set the sprite of the face, must be calld after .square() emitter.spriteBake(0, SPRITES[spriteIdx], MutableQuadView.BAKE_LOCK_UV); // TODO: bake flags? // Enable texture usage emitter.spriteColor(0, -1, -1, -1, -1); emitter.emit(); } mesh = builder.build(); return this; }
''BakedModel'' methods
TODO: check this The methods here are not used by the Fabric Renderer, so we don't really care about the implementation.
@Override public List<BakedQuad> getQuads(BlockState state, Direction face, Random random) { return null; // Don't need because we use FabricBakedModel instead } @Override public boolean useAmbientOcclusion() { return false; } @Override public boolean hasDepth() { return false; } @Override public boolean isSideLit() { return false; } @Override public boolean isBuiltin() { return false; } @Override public Sprite getSprite() { return SPRITES[1]; // Block break particle, let's use furnace_top } @Override public ModelTransformation getTransformation() { return null; } @Override public ModelOverrideList getOverrides() { return null; }
''FabricBakedModel'' methods
@Override public boolean isVanillaAdapter() { return false; // False to trigger FabricBakedModel rendering } @Override public void emitBlockQuads(BlockRenderView blockRenderView, BlockState blockState, BlockPos blockPos, Supplier<Random> supplier, RenderContext renderContext) { // Render function // We just render the mesh renderContext.meshConsumer().accept(mesh); } @Override public void emitItemQuads(ItemStack itemStack, Supplier<Random> supplier, RenderContext renderContext) { } }
Registering the model
Let's first register a ModelResourceProvider
. Add this to your client initializer. TODO add a link explaining this
ModelLoadingRegistry.INSTANCE.registerResourceProvider(rm -> new TutorialModelProvider());
Let's register the model under the name tutorial:block/four_sided_furnace
using the model provider. TODO explain this
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; } } }
Wrapping up
You can now register your block to use our new model, for example if your block only has one block state, put this in `assets/your_mod_id/blockstates/your_block_id.json`.
{ "variants": { "": { "model": "tutorial:block/four_sided_furnace" } } } Of course, you can implement much more complex behavior!