User Tools

Site Tools


tutorial:custom_model

This is an old revision of the document!


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.

Note that models are rendered when the chunks are rebuilt. If you need more dynamic rendering, you can use a BlockEntityRenderer: blockentityrenderers.

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 (and all its model dependencies, and their dependencies, etc...!) 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 using the Renderer API
        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 called after .square()
            // We haven't specified any UV coordinates, so we want to use the whole texture. BAKE_LOCK_UV does exactly that.
            emitter.spriteBake(0, SPRITES[spriteIdx], MutableQuadView.BAKE_LOCK_UV);
            // Enable texture usage
            emitter.spriteColor(0, -1, -1, -1, -1);
            // Add the quad to the mesh
            emitter.emit();
        }
        mesh = builder.build();
 
        return this;
    }

BakedModel methods

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; // Again, we don't really care, etc...
    }
 
    @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 write a ModelResourceProvider, an interface that allows you to provide an UnbakedModel before the game tries to load it from JSON. Have a look at the documentation for more details. The important part is that loadModelResource() will be called for every model.

Let's register the model under the name tutorial:block/four_sided_furnace.

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;
        }
    }
}

Now we have to register this class in the client initializer, the entry point for client-specific code.

public class ExampleModClient implements ClientModInitializer {
    @Override
    public void onInitializeClient() {
        ModelLoadingRegistry.INSTANCE.registerResourceProvider(rm -> new TutorialModelProvider());
 
        /* Other client-specific initialization */
    }
}

Don't forget to register this entrypoint in fabric.mod.json if you haven't done it yet:

/* ... */
  "entrypoints": {
    /* ... */
    "client": [
      "tutorial.path.to.ExampleModClient"
    ]
  },

Wrapping up

You can now register your block to use your 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 rendering. Have fun!

tutorial/custom_model.1597264003.txt.gz · Last modified: 2020/08/12 20:26 by technici4n