Le format des Blockstates de Forge
-
Sommaire
Introduction
Ce tutoriel a pour but de vous apprendre l’utilisation et les avantages du format JSON de blockstate (“état de bloc”) de forge.
Pré-requis
Code
Il est important d’avoir lu et compris les pré-requis pour pouvoir suivre ce tutoriel.
L’utilisation des blockstates classiques est parfois fastidieux.
Une des limites de celui-ci est que pour un même modèle, si on veut changer la texture pour plusieurs variantes d’un bloc, il faut créer un JSON de modèle par texture possible.La classe du bloc :
Pour donner un exemple d’utilisation, j’ai créé un bloc avec méta-données. Il s’agit d’un bouclier décoratif que l’on peut poser sur un mur. Il n’y a pas de changement à effectuer dans le code pour pouvoir utiliser le format de forge puisque le seul changement et dans les fichiers JSON du modèle du bloc.
Mon bloc possède 3 propriétés :
- BlockHorizontal.FACING pour l’orientation du bloc
- un booléen pour ajouter on non une épée décorative au bouclier :
public static final PropertyBool SWORD = PropertyBool.create("sword");
- une énumération pour définir la forme du bouclier :
public static final PropertyEnum<ShieldShape> SHAPE = PropertyEnum.create("shape", ShieldShape.class); public static enum ShieldShape implements IStringSerializable { ROUND, FANCY; private final String name; private final int meta; private ShieldShape() { this.name = this.name().toLowerCase(); //Ici j'utilise name() et ordinal() qui sont des méthodes disponibles dans tous les enum, mais vous pouvez aussi passer les valeurs en paramètres. this.meta = this.ordinal(); } @Override public String getName() { return this.name; } public static final int MAX_META = values().length; private static final ShieldShape[] META_LOOKUP = values(); public static ShieldShape byMeta(int meta) { return META_LOOKUP[meta % MAX_META]; } }
La classe de mon bloc contient aussi toutes les méthodes qui permettent le fonctionnement du bloc, mais je ne vais pas les détailler puisqu’elles sont expliquées dans les tutoriels pré-requis.
Voici la classe complète :public class ShieldBlock extends BlockHorizontal { public static final PropertyBool SWORD = PropertyBool.create("sword"); public static final PropertyEnum<ShieldShape> SHAPE = PropertyEnum.create("shape", ShieldShape.class); // La boite de collision de mon bloc en fonction de la direction protected static final AxisAlignedBB[] SHIELD_AABB = {new AxisAlignedBB(0.0D, 0.0D, 0.0D, 1.0D, 1.0D, 0.1875D), //South new AxisAlignedBB(0.8125D, 0.0D, 0.0D, 1.0D, 1.0D, 1.0D), //West new AxisAlignedBB(0.0D, 0.0D, 0.8125D, 1.0D, 1.0D, 1.0D), //North new AxisAlignedBB(0.0D, 0.0D, 0.0D, 0.1875D, 1.0D, 1.0D)};//East public ShieldBlock() { super(Material.CIRCUITS); this.setCreativeTab(CreativeTabs.DECORATIONS); this.setDefaultState(this.blockState.getBaseState().withProperty(FACING, EnumFacing.NORTH).withProperty(SHAPE, ShieldShape.ROUND).withProperty(SWORD, false)); } @Override public IBlockState getStateForPlacement(World worldIn, BlockPos pos, EnumFacing facing, float hitX, float hitY, float hitZ, int meta, EntityLivingBase placer) { // Si le joueur place le bloc au sol au au plafond, la rotation va être la rotation du joueur if (facing.getAxis() == EnumFacing.Axis.Y) facing = placer.getHorizontalFacing().getOpposite(); return this.getStateFromMeta(meta).withProperty(FACING, facing); } // Pour désactiver le culling @Override public boolean isOpaqueCube(IBlockState state) { return false; } @Override public boolean isFullCube(IBlockState state) { return false; } @Override public AxisAlignedBB getBoundingBox(IBlockState state, IBlockAccess source, BlockPos pos) { return SHIELD_AABB[state.getValue(FACING).getHorizontalIndex()]; } @Override public int damageDropped(IBlockState state) { // L'item du bloc ne prends en compte que la forme et s'il y a l'épée. Pas de la rotation return state.getValue(SHAPE).meta | (state.getValue(SWORD) ? 2 : 0); } @Override public BlockFaceShape getBlockFaceShape(IBlockAccess worldIn, IBlockState state, BlockPos pos, EnumFacing face) { return BlockFaceShape.UNDEFINED; } // Ajouter toutes les variantes dans le menu créatif @Override public void getSubBlocks(CreativeTabs itemIn, NonNullList<ItemStack> items) { for (int i = 0; i < 4; ++i) { items.add(new ItemStack(this, 1, i)); } } @Override public IBlockState getStateFromMeta(int meta) { return this.getDefaultState() .withProperty(SHAPE , ShieldShape.byMeta(meta)) .withProperty(SWORD , (meta >> 1) > 0) .withProperty(FACING, EnumFacing.getHorizontal(meta >> 2)); } @Override public int getMetaFromState(IBlockState state) { return state.getValue(SHAPE).meta | (state.getValue(SWORD) ? 2 : 0) | (state.getValue(FACING).getHorizontalIndex() << 2); } @Override protected BlockStateContainer createBlockState() { return new BlockStateContainer(this, SHAPE, SWORD, FACING); } public static enum ShieldShape implements IStringSerializable { ROUND, FANCY; private final String name; private final int meta; private ShieldShape() { this.name = this.name().toLowerCase(); this.meta = this.ordinal(); } @Override public String getName() { return this.name; } public static final int MAX_META = values().length; private static final ShieldShape[] META_LOOKUP = values(); public static ShieldShape byMeta(int meta) { return META_LOOKUP[meta % MAX_META]; } } }
La classe des blocs et la classe principale :
Pas de changement
Le JSON de blockstate:
Passons à l’objet du tutoriel :
Lorsque l’on crée un JSON, il faut indiquer qu’il utilise le format de forge en ajoutant un marqueur :
{ "forge_marker": 1, // Le reste du fichier va ici }
Ici, j’ai mis le marqueur à 1, mais cela correspond à la version utilisée. Pour le moment il n’y a qu’une version, mais si d’autres versions sont ajoutées, c’est ici que vous allez pouvoir choisir la version que vois utilisez.
Les blockstates de forge fonctionnent par couches successives :- Il y a le fond, ce qui est affiché par défaut s’il n’y a pas de couche au-dessus pour la modifier.
- Il y a les variantes : elles vont modifier le résultat de la couche précédente et sont actives seulement si le bloc rendu possède les bonnes propriétés.
Le format est donc le suivant :
{ "forge_marker": 1, "defaults": { // Les paramètres par défaut du modèle }, "variants": { // Les différentes modifications à apporter aux paramètres du modèle } }
A noter que l’on n’est pas obligé de mettre de paramètre par défaut si les paramètres obligatoires sont définis dans toutes les variantes possibles.
Les paramètres simples :
Nous allons commencer par préciser le modèle par défaut de notre bloc :
{ "forge_marker": 1, "defaults": { "model": "maxample:shield_round" }, "variants": { } }
Le modèle que j’utilise ici n’est pas “complet” c’est-à-dire qu’il n’est pas utilisable tel-quel dans les JSON de blockstate classiques puisque certaines textures n’ont pas été définies :
{ "textures": { "particle": "#shield", // Ici, il y a habituellement le chemin vers la texture. "shield": "#shield", //A la place, il y a #shield ou #back. Ce sont des textures qu'il faut redéfinir. "back": "#back" }, "elements": [ // Définition de mon modèle. Ceci est top secret. ] }
Pour cela nous allons donc définir les textures avec le tag textures :
{ "forge_marker": 1, "defaults": { "model": "maxample:shield_round", "textures": { "shield": "maxample:items/wood_shield", "back": "maxample:items/shield_back" } }, "variants": { } }
On associe à chaque nom, la texture qui lui correspond.
Tout comme le format classique de blockstate, il est possible d’effectuer des rotations du modèle avec les tags x et y et de bloquer la rotation de la texture avec uvlock :{ "forge_marker": 1, "defaults": { "model": "maxample:shield_round", "textures": { "shield": "maxample:items/wood_shield", "back": "maxample:items/shield_back" }, "x": 90, "y": 270, "uvlock": true }, "variants": { } }
En plus de cela, il est possible de désactiver l’occlusion ambiante et l’éclairage adouci avec le tag smooth_lighting:
{ "forge_marker": 1, "defaults": { "model": "maxample:shield_round", "textures": { "shield": "maxample:items/wood_shield", "back": "maxample:items/shield_back" }, "smooth_lighting": false }, "variants": { } }
Les variantes :
J’aimerais que la rotation de mon bloc dépende de la propriété BlockHorizontal.FACING de celui-ci. Pour cela nous allons utiliser les variantes.
La propriété s’appelle “facing” et a 4 valeurs possibles : “north”, “south”, “east” ou “west”.{ "forge_marker": 1, "defaults": { ... }, "variants": { "facing": { "north": { }, "south": { }, "east": { }, "west": { } } } }
On doit mettre un tag avec le nom de la propriété et à l’intérieur, un tag par valeur.
Dans les accolades de chaque valeur, on peut mettre les propriétés appliquées lorsque la propriété du bloc (ici “facing”) est à cette valeur :{ "forge_marker": 1, "defaults": { ... }, "variants": { "facing": { "north": { }, "south": { "y": 180 }, "west": { "y": 270 }, "east": { "y": 90 } } } }
De cette manière, lorsque la valeur de la propriété “facing” du bloc sera “south”, le bloc aura une rotation de 180° et de même pour les autres directions.
Toutes les valeurs doivent apparaitre même si elles n’apportent pas de modification aux paramètres du modèle, donc la valeur “north” doit être présent même si elle n’apporte aucun changement dans le modèle.
Il faut aussi ajouter les autres propriétés et leurs valeurs pour que le JSON soit valide :{ "forge_marker": 1, "defaults": { "model": "maxample:shield_round", "textures": { "shield": "maxample:items/wood_shield", "back": "maxample:items/shield_back" } }, "variants": { "shape": { "round": { }, "fancy": { "model": "maxample:shield_fancy" } }, "sword": { "false": { "textures": { "shield": "maxample:items/wood_shield" } }, "true": { "textures": { "shield": "maxample:items/legendary_shield" } } }, "facing": { ... } } }
Comme je l’ai dit précédemment, les variantes sont prioritaires sur les paramètres par défaut. Donc si la propriété “shape” a pour valeur “fancy”, le modèle utilisé sera “maxample:shield_fancy” à la place de “maxample:shield_round”.
Voici à quoi ressemble mon bloc déjà :
Il y a des cas où les variantes ne sont pas des propriétés. Par exemple si un bloc n’a pas de propriétés, il utilisera la variante “normal” par défaut et les items utilisent généralement la variante “inventory”. Dans ce cas, il est possible de définir une ou plusieurs listes de propriétés de modèle et une seule de ces listes sera choisie aléatoirement en fonction de la position du bloc.
Par exemple, le bloc de terre a ce fichier de blockstate dans Minecraft vanilla :
{ "variants": { "normal": [ { "model": "dirt" }, { "model": "dirt", "y": 90 }, { "model": "dirt", "y": 180 }, { "model": "dirt", "y": 270 } ] } }
Cela peut être fait de cette manière avec le format de forge :
{ "forge_marker": 1, "defaults": { "model": "block/cube_all", "textures": { "all": "blocks/dirt" } }, "variants": { "normal": [ { }, { "y": 90 }, { "y": 180 }, { "y": 270 } ] } }
Même si le fichier est plus long, il y a moins de répétitions et il n’y a pas besoin du fichier de modèle du bloc de terre puisque l’on utilise directement le modèle cube_all. Il est possible aussi d’appliquer un poids à une des listes pour quelle soit plus souvent choisie. Par exemple si je mets ces poids :
"normal": [ { "weight": 5 }, { "y": 90, "weight": 1 }, { "y": 180, "weight": 1 }, { "y": 270, "weight": 1 } ]
Il y aura 5 fois plus de chance de n’appliquer aucune rotation que d’effectuer une rotation de 90°.
Les sous-modèles :
Maintenant que le modèle fonctionne, on va pouvoir s’occuper de l’épée. Lorsque la propriété “sword” vaut “true”, j’aimerais que le modèle d’une épée soit affichée en plus du modèle du bouclier. Pour cela, il y a les sous-modèles qui remplacent le tag multipart du format des fichiers de blockstates classique.
Les sous-modèles sont aussi des propriétés mais sont utilisés uniquement dans les variantes (donc pas dans le tag defaults) et permettent de définir les propriétés du modèle ajouté en plus.
Il existe deux types de sous-modèles :- Les sous-modèles “simples” : ils fonctionnent de manière un peu étrange, c’est pour ça que je la déconseille. Au lieu d’appliques la variante sur le modèle par défaut, elle va être appliquée sur le sous-modèle et le tag submodel permet de définir le modèle utilisé par celui-ci.
- Les sous-modèles “complets” : ils fonctionnent de manière plus simple (Même si leur nom indique le contraire). Dans le tag submodel on va pouvoir définir plusieurs sous-modèles. Pour cela on doit choisir un identifiant à chaque sous-modèle :
"submodel": { "identifiant1": { //Propriétés du sous-modèle 1 }, "identifiant2": { //Propriétés du sous-modèle 2 } }
Les propriétés utilisables dans les sous-modèles sont les mêmes que les propriétés des variantes :
"submodel": { "sword": { "model": "maxample:master_sword" // Une épée }, "sword2": { "model": "maxample:master_sword", // Une deuxième épée avec beaucoup de rotations ! "x": 180, "y": 180 } }
A noter que la rotation du modèle principal affecte aussi les sous-modèles. Donc ici, les épées auront la même rotation que le bloc en plus de la rotation x et y pour la deuxième épée.
Voilà, vous connaissez désormais le principal sur le format de forge. Je vous réserve tout de même quelque bonus ;). Je vous mets ici le résultat final de mon fichier de blockstate. J’ai apporté quelques modifications en plus pour que le rendu soit parfait.{ "forge_marker": 1, "defaults": { "model": "maxample:shield_round", "smooth_lighting": false, "textures": { "back": "maxample:items/shield_back" } }, "variants": { "shape": { "round": { }, "fancy": { "model": "maxample:shield_fancy" } }, "sword": { "false": { "textures": { "shield": "maxample:items/wood_shield" } }, "true": { "textures": { "shield": "maxample:items/legendary_shield" }, "submodel": { "sword": { "model": "maxample:master_sword" } } } }, "facing": { "north": { }, "south": { "y": 180 }, "west": { "y": 270 }, "east": { "y": 90 } } } }
Et voilà à quoi ressemble désormais mon magnifique bouclier :
J’aurais bien voulu vous montrer à quoi cela ressemblerait si on utilisait le format vanilla, mais je n’ai pas assez de patience pour ça.
Vous pouvez trouver un résumé du format ici. (Ce n’est pas complet mais c’est quand même pratique)Bonus
Les modèles custom :
Les modèles custom sont des modèles qui ne sont pas liés à un fichier de modèle JSON. Ils sont directement liés à du code. Il est possible d’en définir soit même, mais ici je vais vous présenter ceux que forge met à notre disposition. Ces modèles s’utilisent de la même manière que les autres modèles et uniquement dans les fichier de blockstate de forge.
Pour les blocs, il existe deux modèles custom de forge :
- “forge:multi-layer” : il permet d’afficher des modèles qui ont des textures transparentes et modèles non transparentes en même temps. Si vous mélangez textures transparentes et non transparentes dans un même modèle sans passer par ça, vous aurez des bugs qui peuvent être différents selon votre code. Par exemple, la texture transparente peut s’afficher complètement opaque ou alors le rendu peut être correcte mais les textures s’affichent dans le mauvais ordre.
Pour utiliser ce modèle, il faudra utiliser la propriété custom dans votre fichier de blockstate. Dans cette propriété qui fonctionne presque comme textures, vous allez pouvoir choisir le modèle à rendre pour chaque couche de rendu : Solid, Mipped Cutout, Cutout, Translucent. Il y a une couche supplémentaire appelé base qui est la seule couche obligatoire. Elle n’est pas rendue pour le bloc, mais elle permet de définir la texture des particules du bloc et d’autres propriétés comme l’occlusion ambiante par exemple. Quand le bloc est rendu sous la forme d’un item, toutes les couches vont être rendues (la couche base aussi). Par contre, le bloc par défaut limite les couches qui peuvent être affichées. Pour enlever cette limitation, vous allez devoir surcharger la méthode canRenderInLayer et retourner true pour les couches que vous voulez utiliser.
Par exemple pour faire une lanterne qui utilise du verre coloré, il vaut mieux passer par ce modèle custom. Par exemple voici une lanterne simple :
{ "forge_marker": 1, "defaults": { "model": "forge:multi-layer", "custom": { "base": "normal_torch", "Solid": "normal_torch" // Modèle de minecraft de la torche } }, "variants": { "color": { "red": { "custom": { "Translucent": "glass_red" // Modèle de minecraft du verre coloré } }, "green": { "custom": { "Translucent": "glass_green" } }, "blue": { "custom": { "Translucent": "glass_blue" } } } } }
Et la méthode canRenderInLayer de mon bloc doit être redéfinie pour accepter les couches “Solid” et “Translucent” :
@Override public boolean canRenderInLayer(IBlockState state, BlockRenderLayer layer) { return layer == BlockRenderLayer.SOLID || layer == BlockRenderLayer.TRANSLUCENT; }
- “forge:fluid” : il permet de créer un modèle de liquide qui s’adapte à vitre fluide. Il suffit de spécifier dans le tag custom le nom de votre fluide et forge s’occupe de tout ! Voici un modèle qui permet de prendre en charge tous les modèles de vos fluides en un seul fichier JSON :
{ "forge_marker": 1, "defaults": { "model": "forge:fluid" }, "variants": { "nom_du_fluid1" : [{ "custom": { "fluid": "nom_du_fluid1" } }], "nom_du_fluid2" : [{ "custom": { "fluid": "nom_du_fluid2" } }], ... } }
Et pour utiliser ce fichier, lors de l’enregistrement du rendu de votre bloc, vous devez faire en sorte de toujours utiliser ce fichier grâce à la méthode setCustomStateMapper :
final ModelResourceLocation resLoc = new ModelResourceLocation("mon_mod_id:nom_du_fichier_json_des_fluids", "nom_du_fluid"); ModelLoader.setCustomStateMapper(mon_bloc_de_fluid, new StateMapperBase() { @Override protected ModelResourceLocation getModelResourceLocation(IBlockState state) { return resLoc; } });
Pour les items :
Malgré leur nom, les fichiers de “blockstate” peuvent aussi être utilisés pour les items ! C’est le cas pour le format vanilla et celui de forge. Par défaut, les items utilisent le modèle d’item qui correspond à leur ResourceLocation. Mais s’il n’existe pas, ils vont chercher la variante “inventory” du blockstate qui correspond.
Pour un bloc simple, on n’a donc juste besoin d’ajouter la variante inventory :{ "variants": { "normal": { "model": "mon_mod_id:mon_modèle" }, "inventory": { "model": "mon_mod_id:mon_modèle" } } }
Comme on le voit, cela ne permet de gérer qu’un seul modèle pour l’item. Mais dans certains cas, on aimerait que le modèle s’adapte aux méta-données de l’item. Pour cela, il va falloir changer le rendu l’item dans le code :
ModelLoader.setCustomModelResourceLocation(item, meta, new ModelResourceLocation("mod_id:nom_du_fichier_json", "nom_de_la_variante"));
C’est assez abstrait, donc je vais vous donner un exemple un peu plus concret pour que vous puissiez comprendre. Pour mon bouclier, il y a un item avec 4 valeurs de méta-données différentes : il y a deux formes différentes et pour chaque forme, il peut y avoir une épée ou non.
J’ai donc besoin d’appeler 4 fois setCustomModelResourceLocation :public static void registerRenders(ModelRegistryEvent event) { Item shieldItem = Item.getItemFromBlock(SHIELD_BLOCK); ModelLoader.setCustomModelResourceLocation(shieldItem, 0, new ModelResourceLocation(SHIELD_BLOCK.getRegistryName(), "facing=north,shape=round,sword=false")); ModelLoader.setCustomModelResourceLocation(shieldItem, 1, new ModelResourceLocation(SHIELD_BLOCK.getRegistryName(), "facing=north,shape=fancy,sword=false")); ModelLoader.setCustomModelResourceLocation(shieldItem, 2, new ModelResourceLocation(SHIELD_BLOCK.getRegistryName(), "facing=north,shape=round,sword=true")); ModelLoader.setCustomModelResourceLocation(shieldItem, 3, new ModelResourceLocation(SHIELD_BLOCK.getRegistryName(), "facing=north,shape=fancy,sword=true")); // On est obligé d'indiquer la valeur de _facing_ même si elle ne change pas, sinon la variante ne va pas être valide. }
Et maintenant, mes items sont presque magnifiques
Ils ont bien le bon modèle, mais ils sont un peu disproportionnés.
Tout comme dans les modèles d’item, il est possible d’ajouter des transformations d’affichage, c’est-à-dire des mis à l’échelle, des rotations ou des déplacements du rendu pour qu’il soit bien placé. Pour cela, il faut utiliser le tag transform dans les propriétés par défaut ou dans une variante. Il y a 8 types d’affichages différents : thirdperson_righthand, thirdperson_lefthand, firstperson_righthand, firstperson_lefthand, head, gui ground, fixed.
Dans le format de forge, on peut soit définir une transformation qui s’appliquera à tous les types d’affichage, soit définir une transformation par type d’affichage :"transform": <une_transformation>
ou
"transform": { "thirdperson_righthand": <une_transformation>, "firstperson_righthand": <une_autre_transformation>, "gui": <une_transformation>, "head": <une_transformation>, ... // On n'est pas obligé de mettre tous les types d'affichage, si on ne précise rien, aucune transformation n'est appliquée. }
Il y a 3 manières de définir une transformation :
- on peut utiliser une transformation par défaut définie par forge en utilisant son nom :
"transform": "identity" // Appliquer aucune transformation "transform": "forge:default-block" // cette transformation est définie dans le fichier de minecraft models/block/block.json "transform": "forge:default-item" "transform": "forge:default-tool" // définie dans le fichier de minecraft models/item/handheld.json
- on peut utiliser les composantes de la transformation :
"transform": { "translation": [<x>, <y>, <z>], "rotation": <une_rotation>, "scale": <multiplicateur>, // Le même multiplicateur pour les 3 coordonnées "scale": [<x>, <y>, <z>], // ou un multiplicateur différent pour chaque coordonnée "post-rotation": <une_autre rotation> // Toutes les composantes ne sont pas obligatoires. }
Ici, une rotation peut être définie par un tableau, ou par un axe et un angle :
"rotation": [<x>, <y>, <z>, <w>] "rotation": {"<axe>": <angle>} "rotation": [{"<axe1>": <angle1>}, {"<axe2>": <angle2>}, ...]
- on peut utiliser une matrice de transformation 4x3 (pour utiliser ça, il faut s’y connaitre) :
"transform": [ [<m00>, <m01>, <m02>, <m03>], [<m10>, <m11>, <m12>, <m13>], [<m20>, <m21>, <m22>, <m23>] ]
Et voila les items positionnés correctement :
Résultat
Ah bah c’est déjà fait, il faut juste remonter pour voir les JSON, le code ou les images. Le code complet sera bientôt disponible sur github.
Crédits
Rédaction :
Correction :
- LeBossMax2 (J’ai relu, ça compte non ?)
Ce tutoriel de LeBossMax2 publié sur Minecraft Forge France est mis à disposition selon les termes de la licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International