🇬🇧 English
🇬🇧 English
Appearance
🇬🇧 English
🇬🇧 English
Appearance
This page is written for version:
1.21.4
This page is written for version:
1.21.4
PREREQUISITES
Make sure you've completed the datagen setup process first.
First, we will need to create our ModelProvider. Create a class that extends FabricModelProvider
. Implement both abstract methods: generateBlockStateModels
and generateItemModels
. Lastly, create a constructor matching super.
public class FabricDocsReferenceModelProvider extends FabricModelProvider {
public FabricDocsReferenceModelProvider(FabricDataOutput output) {
super(output);
}
@Override
public void generateBlockStateModels(BlockStateModelGenerator blockStateModelGenerator) {
}
@Override
public void generateItemModels(ItemModelGenerator itemModelGenerator) {
}
@Override
public String getName() {
return "FabricDocsReference Model Provider";
}
}
Register this class in your DataGeneratorEntrypoint
within the onInitializeDataGenerator
method.
@Override
public void generateBlockStateModels(BlockStateModelGenerator blockStateModelGenerator) {
}
For block models, we will primarily be focusing on the generateBlockStateModels
method. Notice the parameter BlockStateModelGenerator blockStateModelGenerator
- this object will be responsible for generating all the JSON files. Here are some handy examples you can use to generate your desired models:
blockStateModelGenerator.registerSimpleCubeAll(ModBlocks.STEEL_BLOCK);
This is the most commonly used function. It generates a JSON model file for a normal cube_all
block model. One texture is used for all six sides, in this case we use steel_block
.
{
"parent": "minecraft:block/cube_all",
"textures": {
"all": "fabric-docs-reference:block/steel_block"
}
}
It also generates a blockstate JSON file. Since we have no blockstate properties (e.g. Axis, Facing, ...), one variant is sufficient, and is used every time the block is placed.
{
"variants": {
"": {
"model": "fabric-docs-reference:block/steel_block"
}
}
}
The registerSingleton
method provides JSON model files based on the TexturedModel
you pass in and a single blockstate variant.
blockStateModelGenerator.registerSingleton(ModBlocks.PIPE_BLOCK, TexturedModel.END_FOR_TOP_CUBE_COLUMN);
This method will generate models for a normal cube, that uses the texture file pipe_block
for the sides and the texture file pipe_block_top
for the top and bottom sides.
{
"parent": "minecraft:block/cube_column",
"textures": {
"end": "fabric-docs-reference:block/pipe_block_top",
"side": "fabric-docs-reference:block/pipe_block"
}
}
TIP
If you're stuck choosing which TextureModel
you should use, open the TexturedModel
class and look at the TextureMaps
!
blockStateModelGenerator.registerCubeAllModelTexturePool(ModBlocks.RUBY_BLOCK)
.stairs(ModBlocks.RUBY_STAIRS)
.slab(ModBlocks.RUBY_SLAB)
.fence(ModBlocks.RUBY_FENCE);
Another useful method is registerCubeAllModelTexturePool
: define the textures by passing in the "base block", and then append the "children", which will have the same textures. In this case, we passed in the RUBY_BLOCK
, so the stairs, slab and fence will use the RUBY_BLOCK
texture.
WARNING
It will also generate a simple cube all JSON model for the "base block" to ensure that it has a block model.
Be aware of this, if you're changing block model of this particular block, as it will result in en error.
You can also append a BlockFamily
, which will generate models for all of its "children".
public static final BlockFamily RUBY_FAMILY =
new BlockFamily.Builder(ModBlocks.RUBY_BLOCK)
.stairs(ModBlocks.RUBY_STAIRS)
.slab(ModBlocks.RUBY_SLAB)
.fence(ModBlocks.RUBY_FENCE)
.build();
blockStateModelGenerator.registerCubeAllModelTexturePool(ModBlocks.RUBY_BLOCK).family(ModBlocks.RUBY_FAMILY);
blockStateModelGenerator.registerDoor(ModBlocks.RUBY_DOOR);
blockStateModelGenerator.registerTrapdoor(ModBlocks.RUBY_TRAPDOOR);
// blockStateModelGenerator.registerOrientableTrapdoor(ModBlocks.RUBY_TRAPDOOR);
Doors and trapdoors are a little different. Here, you have to make three new textures - two for the door, and one for the trapdoor.
ruby_door_top
for the upper half and ruby_door_bottom
for the lower.registerDoor()
method will create models for all orientations of the door, both open and closed.assets/mod_id/textures/item/
folder.ruby_trapdoor
. It will be used for all sides.TrapdoorBlock
has a FACING
property, you can use the commented out method to generate model files with rotated textures = the trapdoor will be "orientable". Otherwise, it will look the same no matter the direction it's facing.In this section, we'll create the models for a Vertical Oak Log Slab, with Oak Log textures.
Points 2. - 6. are declared in an inner static helper class called CustomBlockStateModelGenerator
.
Create a VerticalSlab
block with a FACING
property and a SINGLE
boolean property, like in the Block States tutorial. SINGLE
will indicate if there are both slabs. Then you should override getOutlineShape
and getCollisionShape
, so that the outline is rendered correctly, and the block has the correct collision shape.
public static final VoxelShape NORTH_SHAPE = Block.createCuboidShape(0.0, 0.0, 0.0, 16.0, 16.0, 8.0);
public static final VoxelShape SOUTH_SHAPE = Block.createCuboidShape(0.0, 0.0, 8.0, 16.0, 16.0, 16.0);
public static final VoxelShape WEST_SHAPE = Block.createCuboidShape(0.0, 0.0, 0.0, 8.0, 16.0, 16.0);
public static final VoxelShape EAST_SHAPE = Block.createCuboidShape(8.0, 0.0, 0.0, 16.0, 16.0, 16.0);
@Override
protected VoxelShape getSidesShape(BlockState state, BlockView world, BlockPos pos) {
boolean type = state.get(SINGLE);
Direction direction = state.get(FACING);
VoxelShape voxelShape;
if (type) {
switch (direction) {
case WEST -> voxelShape = WEST_SHAPE.asCuboid();
case EAST -> voxelShape = EAST_SHAPE.asCuboid();
case SOUTH -> voxelShape = SOUTH_SHAPE.asCuboid();
case NORTH -> voxelShape = NORTH_SHAPE.asCuboid();
default -> throw new MatchException(null, null);
}
return voxelShape;
} else {
return VoxelShapes.fullCube();
}
}
@Override
protected VoxelShape getOutlineShape(BlockState state, BlockView world, BlockPos pos, ShapeContext context) {
return this.getSidesShape(state, world, pos);
}
@Override
protected VoxelShape getCollisionShape(BlockState state, BlockView world, BlockPos pos, ShapeContext context) {
return this.getSidesShape(state, world, pos);
}
Also override the canReplace()
method, otherwise you couldn't make the slab a full block.
@Override
protected boolean canReplace(BlockState state, ItemPlacementContext context) {
Direction direction = state.get(FACING);
if (context.getStack().isOf(this.asItem()) && state.get(SINGLE)) {
if (context.canReplaceExisting()) {
return context.getSide().getOpposite() == direction;
}
}
return false;
}
And you're done! You can now test the block out and place it in game.
Now, let's create a parent block model. It will determine the size, position in hand or other slots and the x
and y
coordinates of the texture. It's recommended to use an editor such as Blockbench for this, as making it manually is a really tedious process. It should look something like this:
{
"parent": "minecraft:block/block",
"textures": {
"particle": "#side"
},
"display": {
"gui": {
"rotation": [
30,
-135,
0
],
"translation": [
-1.5,
0.75,
0
],
"scale": [
0.625,
0.625,
0.625
]
},
"firstperson_righthand": {
"rotation": [
0,
-45,
0
],
"translation": [
0,
2,
0
],
"scale": [
0.375,
0.375,
0.375
]
},
"firstperson_lefthand": {
"rotation": [
0,
315,
0
],
"translation": [
0,
2,
0
],
"scale": [
0.375,
0.375,
0.375
]
},
"thirdperson_righthand": {
"rotation": [
75,
-45,
0
],
"translation": [
0,
0,
2
],
"scale": [
0.375,
0.375,
0.375
]
},
"thirdperson_lefthand": {
"rotation": [
75,
315,
0
],
"translation": [
0,
0,
2
],
"scale": [
0.375,
0.375,
0.375
]
}
},
"elements": [
{
"from": [
0,
0,
0
],
"to": [
16,
16,
8
],
"faces": {
"down": {
"uv": [
0,
8,
16,
16
],
"texture": "#bottom",
"cullface": "down",
"tintindex": 0
},
"up": {
"uv": [
0,
0,
16,
8
],
"texture": "#top",
"cullface": "up",
"tintindex": 0
},
"north": {
"uv": [
0,
0,
16,
16
],
"texture": "#side",
"cullface": "north",
"tintindex": 0
},
"south": {
"uv": [
0,
0,
16,
16
],
"texture": "#side",
"tintindex": 0
},
"west": {
"uv": [
0,
0,
8,
16
],
"texture": "#side",
"cullface": "west",
"tintindex": 0
},
"east": {
"uv": [
8,
0,
16,
16
],
"texture": "#side",
"cullface": "east",
"tintindex": 0
}
}
}
]
}
See how blockstates are formatted for more information. Notice the #bottom
, #top
, #side
keywords. They act as variables that can be set by models that have this one as a parent:
{
"parent": "minecraft:block/cube_bottom_top",
"textures": {
"bottom": "minecraft:block/sandstone_bottom",
"side": "minecraft:block/sandstone",
"top": "minecraft:block/sandstone_top"
}
}
The bottom
value will replace the #bottom
placeholder and so on. Put it in the resources/assets/mod_id/models/block/
folder.
Another thing we will need is an instance of the Model
class. It will represent the actual parent block model inside our mod.
public static final Model VERTICAL_SLAB = block("vertical_slab", TextureKey.BOTTOM, TextureKey.TOP, TextureKey.SIDE);
//helper method for creating Models
private static Model block(String parent, TextureKey... requiredTextureKeys) {
return new Model(Optional.of(Identifier.of(FabricDocsReference.MOD_ID, "block/" + parent)), Optional.empty(), requiredTextureKeys);
}
//helper method for creating Models with variants
private static Model block(String parent, String variant, TextureKey... requiredTextureKeys) {
return new Model(Optional.of(Identifier.of(FabricDocsReference.MOD_ID, "block/" + parent)), Optional.of(variant), requiredTextureKeys);
}
The block()
method creates a new Model
, pointing to the vertical_slab.json
file inside the resources/assets/mod_id/models/block/
folder. The TextureKey
s represent the "placeholders" (#bottom
, #top
, ...) as an Object.
What does TextureMap
do? It actually provides the Identifiers that point to the textures. It technically behaves like a normal map - you associate a TextureKey
(Key) with an Identifier
(Value).
You can either use the vanilla ones, like TextureMap.all()
(which associates all TextureKeys with the same Identifier), or create a new one, by creating a new instance and then using .put()
to associate keys with values.
TIP
TextureMap.all()
associates all TextureKeys with the same Identifier, no matter how many of them there are!
Since we want to use the Oak Log textures, but have the BOTTOM
, TOP
and SIDE
TextureKey
s, we need to create a new one.
public static TextureMap blockAndTopForEnds(Block block) {
return new TextureMap()
.put(TextureKey.TOP, ModelIds.getBlockSubModelId(block, "_top"))
.put(TextureKey.BOTTOM, ModelIds.getBlockSubModelId(block, "_top"))
.put(TextureKey.SIDE, ModelIds.getBlockModelId(block));
}
The bottom
and top
faces will use oak_log_top.png
, the sides will use oak_log.png
.
WARNING
All TextureKey
s in the TextureMap have to match all TextureKey
s in your parent block model!
BlockStateSupplier
Method The BlockStateSupplier
contains all blockstate variants, their rotation, and other options like uvlock.
private static BlockStateSupplier createVerticalSlabBlockStates(Block vertSlabBlock, Identifier vertSlabId, Identifier fullBlockId) {
VariantSetting<Boolean> uvlock = VariantSettings.UVLOCK;
VariantSetting<VariantSettings.Rotation> yRot = VariantSettings.Y;
return VariantsBlockStateSupplier.create(vertSlabBlock).coordinate(BlockStateVariantMap.create(VerticalSlabBlock.FACING, VerticalSlabBlock.SINGLE)
.register(Direction.NORTH, true, BlockStateVariant.create().put(VariantSettings.MODEL, vertSlabId).put(uvlock, true))
.register(Direction.EAST, true, BlockStateVariant.create().put(VariantSettings.MODEL, vertSlabId).put(uvlock, true).put(yRot, VariantSettings.Rotation.R90))
.register(Direction.SOUTH, true, BlockStateVariant.create().put(VariantSettings.MODEL, vertSlabId).put(uvlock, true).put(yRot, VariantSettings.Rotation.R180))
.register(Direction.WEST, true, BlockStateVariant.create().put(VariantSettings.MODEL, vertSlabId).put(uvlock, true).put(yRot, VariantSettings.Rotation.R270))
.register(Direction.NORTH, false, BlockStateVariant.create().put(VariantSettings.MODEL, fullBlockId).put(uvlock, true))
.register(Direction.EAST, false, BlockStateVariant.create().put(VariantSettings.MODEL, fullBlockId).put(uvlock, true))
.register(Direction.SOUTH, false, BlockStateVariant.create().put(VariantSettings.MODEL, fullBlockId).put(uvlock, true))
.register(Direction.WEST, false, BlockStateVariant.create().put(VariantSettings.MODEL, fullBlockId).put(uvlock, true)));
}
First, we create a new VariantsBlockStateSupplier
using VariantsBlockStateSupplier.create()
. Then we create a new BlockStateVariantMap
that contains parameters for all variants of the block, in this case FACING
and SINGLE
, and pass it into the VariantsBlockStateSupplier
. Specify which model and which transformations (uvlock, rotation) is used when using .register()
. For example:
The last step - creating an actual method you can call and that will generate the JSONs. But what are the parameters for?
BlockStateModelGenerator generator
, the same one that got passed into generateBlockStateModels
.Block vertSlabBlock
is the block to which we will generate the JSONs.Block fullBlock
- is the model used when the SINGLE
property is false = the slab block looks like a full block.TextureMap textures
defines the actual textures the model uses. See the Using Texture Map chapter.public static void registerVerticalSlab(BlockStateModelGenerator generator, Block vertSlabBlock, Block fullBlock, TextureMap textures) {
Identifier slabModel = VERTICAL_SLAB.upload(vertSlabBlock, textures, generator.modelCollector);
Identifier fullBlockModel = ModelIds.getBlockModelId(fullBlock);
generator.blockStateCollector.accept(createVerticalSlabBlockStates(vertSlabBlock, slabModel, fullBlockModel));
generator.registerParentedItemModel(vertSlabBlock, slabModel);
}
First, we get the Identifier
of the single slab model with VERTICAL_SLAB.upload()
. Then we get the Identifier
of the full block model with ModelIds.getBlockModelId()
, and pass those two models into createVerticalSlabBlockStates
. The BlockStateSupplier
gets passed into the blockStateCollector
, so that the JSON files are actually generated. Also, we create a model for the vertical slab item with BlockStateModelGenerator.registerParentedItemModel()
.
And that is all! Now all that's left to do is to call our method in our ModelProvider
:
CustomBlockStateModelGenerator.registerVerticalSlab(
blockStateModelGenerator,
ModBlocks.VERTICAL_OAK_LOG_SLAB,
Blocks.OAK_LOG,
CustomBlockStateModelGenerator.blockAndTopForEnds(Blocks.OAK_LOG)
);
You can view the example tests in Fabric API and this documentation's Reference Mod for more information.
You can also find more examples of using custom datagen methods by browsing mods' open-source code, for example Vanilla+ Blocks and Vanilla+ Verticals by Fellteros.