Ce tutoriel est également disponible en vidéo.
Sommaire
Introduction
Dans ce tutoriel nous allons ajouter un rendu avancé à un bloc en utilisant la méthode TileEntitySpecialRenderer (TESR).
Cette méthode a plusieurs avantages :
- Elle permet de rendre un modèle fait avec techne.
- Elle permet de faire des animations.
- On peut facilement faire interagir le rendu avec l’entité du bloc en question.
Mais elle a aussi quelques limitations :
- Elle est plus lourde qu’un rendu simple du fait qu’elle est mise à jour à chaque tick. Faire un monde avec un bloc qui a un rendu TESR est donc une très mauvaise idée.
- Elle ne permet pas de faire le rendu dans l’inventaire, mais nous allons contourner ce problème avec un rendu ISBRH.
- Elle nécessite un bloc avec une entité.
Ce type de rendu est donc inévitable si vous souhaitez animer votre bloc ou passer par un modèle Techne. Dans les autres cas un rendu ISBRH sera plus intéressant car il est plus léger.
Pré-requis
- Ajouter une entité à votre bloc (Tile Entity)
- Un modèle Techne
- Rendre son bloc orientable (pour le bonus seulement)
Code
Finalisation du modèle :
SCAREX a créé un petit outil qui fait cette étape automatiquement. Il vous suffit d’entrer le code de votre modèle et mettre le nom de votre package, le script vous donnera le code corrigé. Lien : http://www.scarex.fr/model_corrector.php
Comme vous avez bien suivis les pré-requis, vous avez un modèle exporté au format Java et une texture. Déplacez le fichier .java dans le dossier forge/src/main/java/votre/package/client/ ou glissez/déposez-la directement dans votre package client sur Eclipse.
Malgré le rouge que vous voyez de partout, ça va aller très vite. Commencez par changer la déclaration du package pour cela il suffit de passer la souris sur l’erreur et prendre l’option “Change package declarations to …”. Ensuite corrigez la déclaration de la classe, il faudra aussi rectifier le nom dans le constructeur. Et voilà, toutes les erreurs restantes ne sont dû qu’à un bête problème d’importation, il suffit de faire ctrl + shift + o et de choisir net.minecraft.entity.Entity lorsqu’Eclipse vous demandera quelle classe “Entity” choisir.
Vous pouvez aussi retirer les commentaires (le //field et tout le blabla à propos de techne au dessus de la classe).
Il ne reste qu’une seule erreur à première vu, sur la dernière fonction. Pour faire simple, supprimez toute la fonction setRotationAngles, elle ne sert que dans le cas d’un mob. Du-coup vous allez avoir une erreur sur la fonction render. Modifiez aussi cette fonction, enlevez les deux lignes suivantes :
super.render(entity, f, f1, f2, f3, f4, f5);
setRotationAngles(f, f1, f2, f3, f4, f5);
Autre modification, remplacez la fonction :
public void render(Entity entity, float f, float f1, float f2, float f3, float f4, float f5)
par :
public void renderAll()
vous allez donc avoir une erreur sur chaque morceauDeModele.render(f5); remplacez donc tous les f5 par la valeur 0.0625F (utilisez ctrl + f et la fonction remplace pour aller plus vite ;))
Voilà, le modèle est maintenant prêt, sauf si vous avez utilisé la fonction mirror de techne. En effet techne fait n’importe quoi à ce niveau, si vous l’avez utilisé supprimez tous les morceauDeModele.mirror = true; ou morceauDeModele.mirror = false; présent dans votre code.
Ensuite, repérez les morceaux qui devrait être en miroir (d’où l’intérêt de les nommés et non de laisser le nom par défaut) et remettez
morceauDeModele.mirror = true;
en dessous de :
morceauDeModele = new ModelRenderer(this, valeur, valeur);
Par exemple ceci est correcte et fonctionnera :
leftPlate = new ModelRenderer(this, 0, 16);
leftPlate.mirror = true;
leftPlate.addBox(0F, 0F, 0F, 1, 12, 13);
leftPlate.setRotationPoint(6F, 11F, -7F);
leftPlate.setTextureSize(64, 64);
ceci ne fonctionnera pas :
leftPlate = new ModelRenderer(this, 0, 16);
leftPlate.addBox(0F, 0F, 0F, 1, 12, 13);
leftPlate.setRotationPoint(6F, 11F, -7F);
leftPlate.setTextureSize(64, 64);
leftPlate.mirror = true;
et ceci va causer un NullPointerException (crash) :
leftPlate.mirror = true;
leftPlate = new ModelRenderer(this, 0, 16);
leftPlate.addBox(0F, 0F, 0F, 1, 12, 13);
leftPlate.setRotationPoint(6F, 11F, -7F);
leftPlate.setTextureSize(64, 64);
(et c’est ce que fait techne alors qu’il ne devrait pas si vous avez cochez le .mirror sur un des morceaux)
Modification de la classe du bloc :
Après avoir corrigé les quelques problèmes de techne, nous allons nous attaquer à la classe du bloc. Premièrement sachez que si votre bloc a des métadatas, vous ne pourrez pas faire en sorte que certains metadatas ont un rendu normal et d’autres un rendu spécial. En revanche vous pouvez mettre plusieurs blocs avec un rendu TESR sur la même instance de bloc en utilisant des metadatas différents (jusqu’à 16, ça ne change pas des metadatas normaux).
Vous devrez normalement déjà avoir la fonction hasTileEntity et la fonction createTileEntity. Si ce n’est pas le cas, retournez voir les pré-requis car il vous faut une entité de bloc !
Ajoutez ces trois fonctions :
public boolean isOpaqueCube()
{
return false;
}
public boolean renderAsNormalBlock()
{
return false;
}
public int getRenderType()
{
return -1;
}
La première signale que le bloc n’est pas opaque (donc que ce n’est pas un bloc remplit de 1x1x1), la deuxième signale qu’il ne doit pas être rendu normalement et la dernière dit que le type de rendu est -1 ce qui vaut à rien. Donc le bloc n’aura plus aucun rendu, et c’est voulu car c’est le TileEntitySpecialRenderer qui va se charger de tout (par défaut c’est RenderBlock qui s’en occupe).
Rendu dans le monde :
Nous allons maintenant nous occuper du rendu dans le monde. Rendez-vous dans votre classe ClientProxy. Si vous avez bien suivis nos tutoriels précédents, vous devrez dedans une fonction nommé registerRender avec peut être déjà des choses dedans.
Ajoutez en plus :
ClientRegistry.bindTileEntitySpecialRenderer(TileEntityTutoriel.class, new TileEntityTutorielSpecialRenderer());
Où TileEntityTutoriel est l’entité de votre bloc et TileEntityTutorielSpecialRenderer est une nouvelle classe que nous allons tout de suite créer.
Pour une question d’organisation, je vous conseil de mettre cette classe dans votre package client (car les rendus ne concerne que le client, tout comme le modèle d’ailleurs).
Normalement Eclipse vous aura déjà mit le extends TileEntityInventorySpecialRenderer et la méthode renderTileEntityAt
Et comme je n’aime pas du tout les p_xxxx_x je vous conseil tout de suite de renommer les arguments comme ceci :
public void renderTileEntityAt(TileEntity tile, double x, double y, double z, float partialRenderTick)
C’est déjà mieux
Maintenant dans cette fonction, appelez une autre fonction que vous allez nommer renderTileEntity<nom de votre entity>At avec les mêmes argument, sauf pour TileEntity que vous allez remplacez par votre TileEntity. Bien sûr créez cette fonction, puisqu’elle n’existe pas encore. Ça donne ça :
@Override
public void renderTileEntityAt(TileEntity tile, double x, double y, double z, float partialRenderTick) // la fonction qui était la de base
{
this.renderTileEntityTutorielAt((TileEntityTutoriel)tile, x, y, z, partialRenderTick); // j'appelle ma fonction renderTileEntityTutorielAt en castant TileEntityTutoriel à l'argument tile
}
private void renderTileEntityTutorielAt(TileEntityTutoriel tile, double x, double y, double z, float partialRenderTick) // ma propre fonction
{
}
Et c’est dans la fonction renderTileEntity<nom de votre entity>At que nous allons gérer le rendu.
En dessous de la déclaration de la classe (après l’accolade de la ligne public class …) créez une instance de votre modèle et de votre texture (avec un resource location) public et statique :
public static ModelBlockTutoriel model = new ModelBlockTutoriel();
public static ResourceLocation texture = new ResourceLocation(ModTutoriel.MODID, "textures/models/blocks/model_block_tutoriel.png");
Pour la première ligne je ne pense pas qu’il n’y ait besoin d’explication, remplacez juste par votre modèle.
Pour la deuxième ligne, ResourceLocation a deux arguments, le premier est le modid vous pouvez donc soit écrire directement “votre modid” ou appeler la constante que nous avons fait dans la classe principale.
Ensuite il faut mettre le chemin complet + le nom de la texture + l’extension à partir du dossier forge/src/main/resources/assets/votre modid/
Dans mon cas la texture sera donc ici :
forge/src/main/resources/assets/modtutoriel/textures/models/blocks/ et sera nommé model_block_tutoriel.png
Pour finir, ajoutez dans votre méthode renderTileEntity<nom de votre entity>At tout ceci :
GL11.glPushMatrix(); // ouvre une matrix
GL11.glTranslated(x + 0.5D, y + 1.5D, z + 0.5D); // déplace le bloc sur les coordonnés et le centre
GL11.glRotatef(180F, 0.0F, 0.0F, 1.0F); // met droit le bloc (par défaut il est à l'envers)
this.bindTexture(texture); // affiche la texture
model.renderAll(); // rend le modèle
GL11.glPopMatrix(); // ferme la matrix
Il ne vous reste plu qu’à lancer le jeu et admirer le résultat.
Si jamais vous avez besoin d’ajuster, voici un repère :
(image trouver sur google provenant de ce site, vous pouvez faire un tour dessus, ça parle de 3D avec java)
GL11.glTranslatef(1.0F, 0.0F, 0.0F); déplace de 1 sur l’axe x.
GL11.glTranslatef(0.0F, 1.0F, 0.0F); déplace de 1 sur l’axe y.
GL11.glTranslatef(0.0F, 0.0F, 1.0F); déplace de 1 sur l’axe z.
Vous pouvez translater sur 3 en même temps dans la même fonction.
GL11.glRotatef(90F, 1.0F, 0.0F, 0.0F); effectue une rotation de 90° sur l’axe x.
GL11.glRotatef(90F, 0.0F, 1.0F, 0.0F); effectue une rotation de 90° sur l’axe y.
GL11.glRotatef(90F, 0.0F, 0.0F, 1.0F); effectue une rotation de 90° sur l’axe z.
Tous ces codes sont à mettre avant “model.renderAll();”
Lancez le jeu en debug (cliquez sur le scarabée à côté de la flèche pour lancer normalement) cela vous permettra de voir les changements en directe (pas besoin de relancer le jeu à chaque fois)
Rendu dans l’inventaire :
Vous l’avez surement remarqué, comme la méthode du TESR ne gère pas le rendu dans l’inventaire, c’est devenu un carré tout moche. Vous pouvez très bien modifier la texture de votre bloc pour faire en sorte qu’elle ressemble à votre modèle 3D si cela vous convient (je pense par exemple à faire une texture comme le lit ou la canne à sucre en main, ça rend pas trop mal). Mais avoir un rendu en 3D dans l’inventaire reste le mieux.
J’en profite aussi pour dire qu’il ne faut pas supprimer la texture original du bloc (celle qui est défini avec setBlockTextureName ou avec la fonction registerBlockIcons et getIcon si c’est un bloc avec metadata) car c’est elle qui va être utilisée pour les particules du bloc (celle qu’on voit lorsqu’on tape sur le bloc, le détruit ou lorsqu’on saute dessus).
Je reviens au rendu dans l’inventaire (qui est au passage aussi celui du bloc lorsqu’il est jeté au sol sous la forme d’entité d’item), comme la méthode du TESR ne gère pas ça, nous allons passer par la méthode ISBRH. (ISimpleBlockRenderingHandler)
Allez à nouveau dans la classe du proxy client, et en dessous de la déclaration de la classe ajoutez :
public static int tesrRenderId;
Cet id va correspondre à l’id de rendu pour **tous **vos blocs avec un rendu TESR. Et j’insiste sur le tous, car en effet ce que nous faisons dans cette partie ne sera à faire qu’une fois par mod, et pas une fois par bloc.
Ensuite dans la méthode registerRender(), ajoutez ces deux lignes :
tesrRenderId = RenderingRegistry.getNextAvailableRenderId();
RenderingRegistry.registerBlockHandler(new TESRInventoryRenderer());
La première permet d’attribuer à notre variable tesrRenderId un id qui n’est pas encore occupé par un autre bloc.
La seconde ligne enregistre le rendu ISBRH.
Attention, ne changé pas l’ordre de ces deux lignes, tesrRenderId doit être initialisé avant que le rendu soit enregistré !
Créez la classe TESRInventoryRenderer, Eclipse devrait normalement automatiquement implémenter l’interface ISimpleBlockRenderingHandler et votre classe devrait ressembler à ça :
package fr.minecraftforgefrance.tutoriel.client;
import net.minecraft.block.Block;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.RenderBlocks;
import net.minecraft.world.IBlockAccess;
import cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler;
public class TESRInventoryRenderer implements ISimpleBlockRenderingHandler
{
@Override
public void renderInventoryBlock(Block block, int metadata, int modelId, RenderBlocks renderer)
{
}
@Override
public boolean renderWorldBlock(IBlockAccess world, int x, int y, int z, Block block, int modelId, RenderBlocks renderer)
{
return false;
}
@Override
public boolean shouldRender3DInInventory(int modelId)
{
return false;
}
@Override
public int getRenderId()
{
return 0;
}
}
Il y a quatre méthodes, une que nous n’allons pas toucher, car elle ne nous intéresse pas. Il s’agit de renderWorldBlock, car si vous avez bien comprit le tutoriel, vous savez que le rendu dans le monde est déjà fait par le rendu TESR.
En revanche on peut déjà voir un petit problème, le but ici est de faire le rendu 3D dans l’inventaire, donc la troisième fonction n’est pas bonne par défaut. Remplacez simplement le false par true dans shouldRender3DInInventory.
Ensuite, dans la fonction getRenderId, remplacez le 0 par ClientProxy.tesrRenderId
Dans la fonction renderInventoryBlock faite simplement le rendu du bloc si le bloc est bien le bloc voulu et que le metadata est le bon :
@Override
public void renderInventoryBlock(Block block, int metadata, int modelId, RenderBlocks renderer)
{
if(block == ModTutoriel.blockTutoriel2 && metadata == 0)
{
GL11.glPushMatrix();
GL11.glRotatef(180F, 0.0F, 0.0F, 1.0F);
GL11.glTranslatef(0.0F, -1.0F, 0.0F);
Minecraft.getMinecraft().getTextureManager().bindTexture(TileEntityTutorielSpecialRenderer.texture);
TileEntityTutorielSpecialRenderer.model.renderAll();
GL11.glPopMatrix();
}
}
Et voila ! Le code est exactement le même qu’avant, il y a juste la translation qui est différente et la fonction pour bind la texture.
Si vous avez besoin d’ajuster à nouveau, les fonction GL11.glTranslatef et GL11.glRotatef fonctionne aussi ici.
Attention, il faut bien utiliser les variables qui se trouve dans la classe de votre TileEntitySpecialRenderer plutôt que de créer une nouvelle instance, car il faut appeler cette classe pour éviter un crash.
D’ailleurs en parlant de ça, allez dans la classe de votre TileEntityTutorielSpecialRenderer et ajoutez un constructeur avec ceci dedans :
public NomDeLaClasse() // TileEntityTutorielSpecialRenderer dans mon cas, c'est la classe que nous avons fait dans la partie précédente
{
this.func_147497_a(TileEntityRendererDispatcher.instance);
}
Sans cette ligne votre jeu va crasher.
Et une dernière modification, dans la classe de votre bloc, modifiez le return -1 dans la fonction getRenderType par return ClientProxy.tesrRenderId.
Voila, j’espère que vous avez réussi à suivre ! Comme il y a pleins de modification dans pleins de classes différentes ce tutoriel n’est pas l’un des plus facile.
Lorsque vous ajouterez d’autre rendu TESR à d’autre bloc, il faudra donc reprendre toute la partie 1, 2 et 3 du tutoriel (finalisation du modèle, modification dans la classe du bloc et rendu dans le monde). Par contre inutile de refaire toute cette partie, il vous suffira de mettre la fonction :
@SideOnly(Side.CLIENT)
public int getRenderType()
{
return ClientProxy.tesrRenderId;
}
dans la classe de votre bloc, et dans la fonction renderInventoryBlock de la classe TESRInventoryRenderer ajoutez votre bloc. Par exemple :
@Override
public void renderInventoryBlock(Block block, int metadata, int modelId, RenderBlocks renderer)
{
if(block == ModTutoriel.blockTutoriel2 && metadata == 0)
{
GL11.glPushMatrix();
GL11.glRotatef(180F, 0.0F, 0.0F, 1.0F);
GL11.glTranslatef(0.0F, -1.0F, 0.0F);
Minecraft.getMinecraft().getTextureManager().bindTexture(TileEntityTutorielSpecialRenderer.texture);
TileEntityTutorielSpecialRenderer.model.renderAll();
GL11.glPopMatrix();
}
else if(block == ModTutoriel.unDeuxiemeBlock && metadata == 0) // un deuxième bloc
{
GL11.glPushMatrix();
GL11.glRotatef(180F, 0.0F, 0.0F, 1.0F);
GL11.glTranslatef(0.0F, -1.0F, 0.0F);
Minecraft.getMinecraft().getTextureManager().bindTexture(TileEntityDeuxiemeSpecialRenderer.texture);
TileEntityDeuxiemeSpecialRenderer.model.renderAll();
GL11.glPopMatrix();
}
else if(block == ModTutoriel.unTroisiemeBlock) // un troisième bloc avec metadata
{
if(metadata == 0) // premier metadata
{
GL11.glPushMatrix();
GL11.glRotatef(180F, 0.0F, 0.0F, 1.0F);
GL11.glTranslatef(0.0F, -1.0F, 0.0F);
Minecraft.getMinecraft().getTextureManager().bindTexture(TileEntityTroisiemeMetadata0SpecialRenderer.texture);
TileEntityTroisiemeMetadata0SpecialRenderer.model.renderAll();
GL11.glPopMatrix();
}
if(metadata == 1) // deuxième metadata
{
GL11.glPushMatrix();
GL11.glRotatef(180F, 0.0F, 0.0F, 1.0F);
GL11.glTranslatef(0.0F, -1.0F, 0.0F);
Minecraft.getMinecraft().getTextureManager().bindTexture(TileEntityTroisiemeMetadata1SpecialRenderer.texture);
TileEntityTroisiemeMetadata1SpecialRenderer.model.renderAll();
GL11.glPopMatrix();
}
// etc ...
}
}
Adapter la hitbox :
La hitbox par défaut de Minecraft sera certainement pas adapté à votre modèle. Ce qui va faire que votre personnage va voler sur le bloc, et que lorsque vous allez placer la souris sur le bloc, vous allez avoir un truc énorme. Pour corriger ça, ajouter dans la classe de votre bloc cette méthode :
public void setBlockBoundsBasedOnState(IBlockAccess world, int x, int y, int z)
{
this.setBlockBounds(0.0F, 0.0F, 0.0F, 1.0F, 1.0F, 1.0F);
}
Lancez votre jeu en debug puis il ne vous reste plu qu’à jouer avec ces valeurs. Augmenter les trois première et reduisez les trois dernière jusqu’à avoir ce que vous voulez.
Dans le cas d’un bloc directionnel il est possible qu’en fonction de la direction du bloc la hitbox devra avoir des valeurs différentes. Si c’est le cas, il suffit de faire :
public void setBlockBoundsBasedOnState(IBlockAccess world, int x, int y, int z)
{
TileEntity tile = world.getTileEntity(x, y, z);
if(tile instanceof TileEntityTutoriel)
{
TileEntityTutoriel tileTuto = (TileEntityTutoriel)tile;
switch(tileTuto.getDirection())
{
case 0:
this.setBlockBounds(0.0F, 0.0F, 0.0F, 1.0F, 1.0F, 1.0F); //valeur pour la direction 0
break;
case 1:
this.setBlockBounds(0.0F, 0.0F, 0.0F, 1.0F, 1.0F, 1.0F); //valeur pour la direction 1
break;
case 2:
this.setBlockBounds(0.0F, 0.0F, 0.0F, 1.0F, 1.0F, 1.0F); //valeur pour la direction 2
break;
case 3:
this.setBlockBounds(0.0F, 0.0F, 0.0F, 1.0F, 1.0F, 1.0F); //valeur pour la direction 3
break;
}
}
}
Rien de compliqué. Bien sûr pour voir directement en jeu si c’est bon, il faut d’abord suivre le bonus pour que le rendu se fasse en fonction de la direction.
Bonus : Faire tourner le modèle
Une fois de plus, il faut avoir bien suivis les pré-requis, vous devez avoir un bloc directionnel en passant par l’entité de bloc pour suivre cette partie.
En fait il n’y a rien de compliqué, dans la classe de votre TileEntitySpecialRenderer, dans la fonction render<votre entité de bloc>At avant :
this.bindTexture(texture);
model.renderAll();
ajoutez :
GL11.glRotatef(90F * tile.getDirection(), 0.0F, 1.0F, 0.0F);
Cela va effectuer une rotation de 90 dégré multiplié par la direction sur l’axe y.
Résultat
Voir le commit sur github
Le commit sur github montre clairement où ont été placés les fichiers, ainsi que ce qui a été ajouté et retiré dans le fichier.
Et une petite image :
En vidéo
https://www.youtube.com/watch?v=KMFOGmje40A
Crédits
Rédaction :
Correction :
Ce tutoriel de 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