Guide: Adding Custom Models
This page/section is lacking in some information and needs expanding. Click here to add more. |
This guide will attempt to explain the process of model implementation. Before proceeding please insure you have the following:
- A model you wish to add (OBJ format with triangle geometry)
- A clone of the NTM Github repository
- Java IDE (IntelliJ IDEA, Eclipse)
Before we jump into adding custom models, it will be beneficial to understand the custom rendering pipeline NTM implements and how to correctly use it.
Default Renderers
By default Minecraft Forge 1.7.10 provides several rendering systems:
Block Models (JSON/Baked Models)
This rendering system supports only simple cube shaped models defined in block state JSON files, that cant include animations and are limited to 16x16x16 grid resolution.
Entity Models (ModelBase)
These are Java-coded models that use ModelRenderer and ModelBox interfaces. Models are just defined boxes with rotations in code. Every face has to be manually coded, no visual editors support model creation to this format, so blind coding is required.
Tile Entity Special Renderer (TESR)
This is a custom OpenGL rendering system for tile entities that supports "almost anything" but requires writing of raw OpenGL code, is prone to errors, doesn't support any model formats (except raw OpenGL code) and can cause serious performance issues if not carefully optimized.
These rendering systems simply wouldn't be able to support NTM's custom complex machinery, detailed weapon models, animated parts and high model count. For this reason NTM implements a custom rendering system designed to efficiently load custom models while simplifying model creation.
NTM's Renders
NTM provides several rendering systems for different model types:
Block/Tile Entity Models (HFRWavefrontObject)
Used for machines, decorative blocks and mutli-blocks (machines, most reactors...).
Supports the OBJ format, Named part rendering, VBO optimalization and animations.
Uses the TIleEntitySpecialRenderer + IItemRendererProvider renderers.
Item models (ItemRenderBase)
Used for guns, tools (generally most hand held items).
Provides first/third person, inventory and animation rendering. Also supports muzzle flash attachment points.
Uses the ItemRenderBase (or weapon specific) renderers.
Entity Models (RenderLiving)
Used for projectiles and mobs (drones, missiles, bosses...).
Features a animation system with hitbox management, shadow rendering and partial tick interpolation.
Uses Render subclasses.
Importing your model
1. Adding resources
First we need to add our custom_model.obj file and the corresponding custom_model_texutre.png to our project. The model files live in the src/main/resources/assets/hbm/models/ directory and corresponding subdirectories. The texture files live in the src/main/resources/assets/hbm/textures/models directory and subdirectories.
2. Optimizing models
Once we add our model and texture to the project we need to choose the correct optimalization. We have a few options:
// No optimization - most flexible, used for animated entities
new HFRWavefrontObject(resource)
// VBO - fastest, used for static machines rendered every frame
new HFRWavefrontObject(resource).asVBO()
// Display List - mainly used for items
new HFRWavefrontObject(resource).asDisplayList()
The HFRWavefrontObject has a optional parameter smoothing HFRWavefrontObject(ResourceLocation resource, boolean smoothing) this is useful for when you want the renderer to smoothen the edges of your model (by default its false).
3. Registering models
Once we have chosen the optimalization method we need to register our resource (model). Resources are managed by the ResourceManager.java class, and be registered as follows:
public class ResourceManager {
// ... existing models ...
public static final IModelCustom refinery =
new HFRWavefrontObject(new ResourceLocation(RefStrings.MODID,
"models/machines/models/refinery.obj")).asVBO();
// REGISTER YOUR MODEL HERE:
public static final IModelCustom my_machine =
new HFRWavefrontObject(new ResourceLocation(RefStrings.MODID,
"models/custom_model.obj")).asVBO();
// ... more models ...
}
4. Creating model renderer classes
After we register our model, we need to create a renderer class for it that will specify which texture belongs to this model, how to transform it and much more.
package com.hbm.render.tileentity;
import com.hbm.blocks.ModBlocks;
import com.hbm.lib.RefStrings;
import com.hbm.main.ResourceManager;
import com.hbm.render.item.ItemRenderBase;
import com.hbm.tileentity.machine.TileEntityMyMachine;
import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer;
import net.minecraft.item.Item;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.client.IItemRenderer;
import org.lwjgl.opengl.GL11;
public class RenderMyMachine extends TileEntitySpecialRenderer implements IItemRendererProvider {
// Path to your texture
private static final ResourceLocation TEX =
new ResourceLocation(RefStrings.MODID, "textures/models/machines/custom_model_texture.png");
@Override
public void renderTileEntityAt(TileEntity tileEntity, double x, double y, double z, float partialTicks) {
// Type safety check
if(!(tileEntity instanceof TileEntityMyMachine)) return;
TileEntityMyMachine machine = (TileEntityMyMachine) tileEntity;
// Get block facing direction from metadata
int meta = tileEntity.getBlockMetadata();
// Start transformation matrix
GL11.glPushMatrix();
// Position model at block location
// +0.5 centers it in the block
GL11.glTranslated(x + 0.5, y, z + 0.5);
// Rotate based on block facing
float rotation = 0;
switch(meta) {
case 2: rotation = 0; break; // North
case 3: rotation = 180; break; // South
case 4: rotation = 90; break; // West
case 5: rotation = 270; break; // East
}
GL11.glRotatef(rotation, 0, 1, 0);
// Bind texture
bindTexture(TEX);
// Set OpenGL states
GL11.glEnable(GL11.GL_CULL_FACE); // Don't render back faces
GL11.glDisable(GL11.GL_BLEND); // No transparency
// RENDER THE MODEL!
ResourceManager.my_machine.renderAll();
// Restore matrix
GL11.glPopMatrix();
}
// For item rendering in inventory/hand
@Override
public Item getItemForRenderer() {
return Item.getItemFromBlock(ModBlocks.my_machine);
}
@Override
public IItemRenderer getRenderer() {
return new ItemRenderBase() {
@Override
public void renderInventory() {
// Position for inventory icon
GL11.glTranslated(0, -3, 0);
GL11.glRotated(-90, 0, 1, 0);
GL11.glScaled(10, 10, 10);
}
@Override
public void renderCommon() {
bindTexture(TEX);
ResourceManager.my_machine.renderAll();
}
};
}
}
5. Registering model renderer classes
Next we need to register our newly created class in the ClientProxy.java class, in the registerRenderInfo() method.
public class ClientProxy extends CommonProxy {
@Override
public void registerRenderInfo() {
// ... existing renderer registrations ...
// Register tile entity renderer
ClientRegistry.bindTileEntitySpecialRenderer(
TileEntityMyMachine.class, // Your tile entity class
new RenderMyMachine() // Your renderer class
);
// ... more registrations ...
}
}
Done, your model should now get loaded and displayed after the next build. If a model fails to load correctly or the rendering pipeline throws an exception, refer to the model creation guide.
Common Patterns & Tamplates
Static Machine (without animations)
public class RenderStaticMachine extends TileEntitySpecialRenderer implements IItemRendererProvider {
private static final ResourceLocation TEX =
new ResourceLocation(RefStrings.MODID, "textures/models/machines/static.png");
@Override
public void renderTileEntityAt(TileEntity te, double x, double y, double z, float f) {
GL11.glPushMatrix();
GL11.glTranslated(x + 0.5, y, z + 0.5);
// Simple rotation
GL11.glRotatef(te.getBlockMetadata() * 90, 0, 1, 0);
bindTexture(TEX);
ResourceManager.static_machine.renderAll();
GL11.glPopMatrix();
}
@Override
public Item getItemForRenderer() {
return Item.getItemFromBlock(ModBlocks.static_machine);
}
@Override
public IItemRenderer getRenderer() {
return new ItemRenderBase() {
public void renderInventory() {
GL11.glTranslated(0, -3, 0);
GL11.glRotated(-90, 0, 1, 0);
GL11.glScaled(10, 10, 10);
}
public void renderCommon() {
bindTexture(TEX);
ResourceManager.static_machine.renderAll();
}
};
}
}
Machine with Animated Parts
model structure: body - static base, rotor - spinning part, door - opening door
public class RenderAnimatedMachine extends TileEntitySpecialRenderer implements IItemRendererProvider {
private static final ResourceLocation TEX =
new ResourceLocation(RefStrings.MODID, "textures/models/machines/animated.png");
@Override
public void renderTileEntityAt(TileEntity te, double x, double y, double z, float partialTicks) {
if(!(te instanceof TileEntityAnimatedMachine)) return;
TileEntityAnimatedMachine machine = (TileEntityAnimatedMachine) te;
GL11.glPushMatrix();
GL11.glTranslated(x + 0.5, y, z + 0.5);
// Rotate for facing
int meta = te.getBlockMetadata();
GL11.glRotatef(meta * 90, 0, 1, 0);
bindTexture(TEX);
// Render static body
ResourceManager.animated_machine.renderPart("body");
// Animate rotor with smooth interpolation
if(machine.isRunning()) {
GL11.glPushMatrix();
// Position rotor pivot (adjust to your model)
GL11.glTranslatef(0, 0.5F, 0);
// Smooth rotation using partialTicks
float rotation = (machine.getRotationTicks() + partialTicks) * 10F;
GL11.glRotatef(rotation, 0, 1, 0);
// Move back
GL11.glTranslatef(0, -0.5F, 0);
// Render rotor
ResourceManager.animated_machine.renderPart("rotor");
GL11.glPopMatrix();
}
// Animate door (opens slowly)
GL11.glPushMatrix();
// Door hinge position
GL11.glTranslatef(-0.5F, 0, 0);
// Door angle based on progress
float doorAngle = machine.getDoorProgress() * 90F; // 0-90 degrees
GL11.glRotatef(doorAngle, 0, 1, 0);
// Move back
GL11.glTranslatef(0.5F, 0, 0);
// Render door
ResourceManager.animated_machine.renderPart("door");
GL11.glPopMatrix();
GL11.glPopMatrix();
}
@Override
public Item getItemForRenderer() {
return Item.getItemFromBlock(ModBlocks.animated_machine);
}
@Override
public IItemRenderer getRenderer() {
return new ItemRenderBase() {
public void renderInventory() {
GL11.glTranslated(0, -3, 0);
GL11.glRotated(-90, 0, 1, 0);
GL11.glScaled(10, 10, 10);
}
public void renderCommon() {
bindTexture(TEX);
// Render with door closed for icon
ResourceManager.animated_machine.renderAll();
}
};
}
}
Item/Weapon Models
public class ItemRenderMyGun extends ItemRenderWeaponBase {
@Override
public void setupFirstPerson(ItemStack stack) {
// Position in first person view
GL11.glTranslated(0, 0, 0.8);
GL11.glScaled(0.375, 0.375, 0.375);
GL11.glRotated(5, 0, 1, 0);
}
@Override
public void renderFirstPerson(ItemStack stack) {
bindTexture(TEX);
// Render gun body
ResourceManager.my_gun.renderAllExcept("magazine", "muzzle_flash");
// Only render magazine if loaded
if(hasAmmo(stack)) {
ResourceManager.my_gun.renderPart("magazine");
}
// Render muzzle flash if firing
if(isFiring(stack)) {
GL11.glDisable(GL11.GL_LIGHTING);
GL11.glEnable(GL11.GL_BLEND);
bindTexture(TEX_FLASH);
ResourceManager.my_gun.renderPart("muzzle_flash");
GL11.glDisable(GL11.GL_BLEND);
GL11.glEnable(GL11.GL_LIGHTING);
}
}
@Override
public void setupThirdPerson(ItemStack stack) {
// Position in third person
GL11.glTranslated(0, 0, 0.5);
GL11.glScaled(0.5, 0.5, 0.5);
GL11.glRotated(-90, 0, 1, 0);
}
@Override
public void renderThirdPerson(ItemStack stack) {
bindTexture(TEX);
ResourceManager.my_gun.renderAll();
}
}