tutorial:custom_model
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
tutorial:custom_model [2020/08/12 18:33] – [custom model] Add preview picture technici4n | tutorial:custom_model [2023/12/31 21:40] (current) – [Sprites] Missed `int`s gudenau | ||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== | + | ====== |
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. | 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 '' | ||
===== Creating the model ===== | ===== Creating the model ===== | ||
Line 8: | Line 10: | ||
<code java> | <code java> | ||
+ | @Environment(EnvType.CLIENT) | ||
public class FourSidedFurnaceModel implements UnbakedModel, | public class FourSidedFurnaceModel implements UnbakedModel, | ||
</ | </ | ||
Line 16: | Line 19: | ||
<code java> | <code java> | ||
private static final SpriteIdentifier[] SPRITE_IDS = new SpriteIdentifier[]{ | private static final SpriteIdentifier[] SPRITE_IDS = new SpriteIdentifier[]{ | ||
- | new SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEX, new Identifier(" | + | new SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE, new Identifier(" |
- | new SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEX, new Identifier(" | + | new SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE, new Identifier(" |
}; | }; | ||
- | private Sprite[] | + | private |
+ | |||
+ | // Some constants to avoid magic numbers, these need to match the SPRITE_IDS | ||
+ | private static final int SPRITE_SIDE = 0; | ||
+ | private static final int SPRITE_TOP = 1; | ||
</ | </ | ||
Line 28: | Line 36: | ||
</ | </ | ||
- | ==== '' | + | ==== UnbakedModel methods ==== |
<code java> | <code java> | ||
@Override | @Override | ||
public Collection< | public Collection< | ||
- | return | + | return |
} | } | ||
@Override | @Override | ||
- | public | + | public |
- | | + | // This is related to model parents, it's not required for our use case |
} | } | ||
Line 44: | Line 52: | ||
public BakedModel bake(ModelLoader loader, Function< | public BakedModel bake(ModelLoader loader, Function< | ||
// Get the sprites | // Get the sprites | ||
- | for(int i = 0; i < 2; ++i) { | + | for(int i = 0; i < SPRITE_IDS.length; ++i) { |
- | | + | |
} | } | ||
- | // Build the mesh | + | // Build the mesh using the Renderer API |
Renderer renderer = RendererAccess.INSTANCE.getRenderer(); | Renderer renderer = RendererAccess.INSTANCE.getRenderer(); | ||
MeshBuilder builder = renderer.meshBuilder(); | MeshBuilder builder = renderer.meshBuilder(); | ||
Line 53: | Line 61: | ||
for(Direction direction : Direction.values()) { | for(Direction direction : Direction.values()) { | ||
- | int spriteIdx = direction == Direction.UP || direction == Direction.DOWN ? 1 : 0; | + | |
+ | | ||
// Add a new face to the mesh | // Add a new face to the mesh | ||
emitter.square(direction, | emitter.square(direction, | ||
- | // Set the sprite of the face, must be calld after .square() | + | // Set the sprite of the face, must be called |
- | emitter.spriteBake(0, SPRITES[spriteIdx], | + | // We haven' |
+ | emitter.spriteBake(sprites[spriteIdx], | ||
// Enable texture usage | // Enable texture usage | ||
- | emitter.spriteColor(0, -1, -1, -1, -1); | + | emitter.color(-1, -1, -1, -1); |
+ | // Add the quad to the mesh | ||
emitter.emit(); | emitter.emit(); | ||
} | } | ||
Line 68: | Line 79: | ||
</ | </ | ||
- | ==== '' | + | ==== BakedModel methods ==== |
- | TODO: check this | + | Not all the methods here are used by the Fabric Renderer, so we don't really care about the implementation. |
- | The methods here are not used by the Fabric Renderer, so we don't really care about the implementation. | + | |
<code java> | <code java> | ||
- | @Override | + | |
public List< | public List< | ||
- | | + | // Don't need because we use FabricBakedModel instead. However, it's better to not return null in case some mod decides to call this function. |
+ | return List.of(); | ||
} | } | ||
@Override | @Override | ||
public boolean useAmbientOcclusion() { | public boolean useAmbientOcclusion() { | ||
- | return | + | return |
} | } | ||
@Override | @Override | ||
- | public boolean | + | public boolean |
return false; | return false; | ||
} | } | ||
@Override | @Override | ||
- | public boolean | + | public boolean |
return false; | return false; | ||
} | } | ||
@Override | @Override | ||
- | public boolean | + | public boolean |
return false; | return false; | ||
} | } | ||
@Override | @Override | ||
- | public Sprite | + | public Sprite |
- | return | + | return |
} | } | ||
Line 113: | Line 124: | ||
</ | </ | ||
- | ==== '' | + | ==== FabricBakedModel methods ==== |
<code java> | <code java> | ||
@Override | @Override | ||
Line 125: | Line 136: | ||
| | ||
// We just render the mesh | // We just render the mesh | ||
- | | + | |
} | } | ||
Line 134: | Line 145: | ||
} | } | ||
</ | </ | ||
+ | |||
+ | Note: Make sure you override the '' | ||
===== Registering the model ===== | ===== Registering the model ===== | ||
- | Let's first register a '' | + | In order for the model to be rendered in game we need to register |
<code java> | <code java> | ||
- | ModelLoadingRegistry.INSTANCE.registerResourceProvider(rm -> new TutorialModelProvider()); | + | @Environment(EnvType.CLIENT) |
+ | public class TutorialModelLoadingPlugin impelements ModelLoadingPlugin { | ||
+ | public static final ModelIdentifier FOUR_SIDED_FURNACE_MODEL = new ModelIdentifier(" | ||
+ | |||
+ | @Override | ||
+ | public void onInitializeModelLoader(Context pluginContext) { | ||
+ | // We want to add our model when the models are loaded | ||
+ | pluginContext.modifyModelOnLoad().register((original, | ||
+ | // This is called for every model that is loaded, so make sure we only target ours | ||
+ | if(context.id().equals(FOUR_SIDED_FURNACE_MODEL)) { | ||
+ | return | ||
+ | } else { | ||
+ | // If we don't modify the model we just return the original as-is | ||
+ | return original; | ||
+ | } | ||
+ | }); | ||
+ | } | ||
+ | } | ||
</ | </ | ||
- | Let' | + | Then you need to register the plugin we just created: |
<code java> | <code java> | ||
- | public class TutorialModelProvider | + | @Environment(EnvType.CLIENT) |
- | public static final Identifier FOUR_SIDED_FURNACE_MODEL = new Identifier(" | + | public class ExampleModClient |
@Override | @Override | ||
- | public | + | public |
- | | + | |
- | return | + | |
- | } else { | + | |
- | return null; | + | |
- | | + | |
} | } | ||
} | } | ||
</ | </ | ||
- | ===== Wrapping up ===== | + | Don't forget to register this entrypoint in '' |
- | You can now register your block to use our new model, for example if your block only has one block state, put this in `assets/ | + | <code json> |
+ | /* ... */ | ||
+ | " | ||
+ | /* ... */ | ||
+ | " | ||
+ | " | ||
+ | ] | ||
+ | }, | ||
+ | </ | ||
+ | |||
+ | ===== Using the model ===== | ||
+ | You can now register your block to use your new model. For example, if your block only has one block state, put this in '' | ||
<code json> | <code json> | ||
{ | { | ||
Line 170: | Line 209: | ||
{{: | {{: | ||
+ | ===== Rendering the item ===== | ||
+ | As you can see in the picture, the item is not rendered correctly. Let's fix this. | ||
+ | |||
+ | ==== Updating the model ==== | ||
+ | We will re-use the same model class, with just a small change: | ||
+ | * We will need a '' | ||
+ | |||
+ | We will update our '' | ||
+ | <code java> | ||
+ | // We need to implement getTransformation() and getOverrides() | ||
+ | @Override | ||
+ | public ModelTransformation getTransformation() { | ||
+ | return ModelHelper.MODEL_TRANSFORM_BLOCK; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public ModelOverrideList getOverrides() { | ||
+ | return ModelOverrideList.EMPTY; | ||
+ | } | ||
+ | | ||
+ | // We will also implement this method to have the correct lighting in the item rendering. Try to set this to false and you will see the difference. | ||
+ | @Override | ||
+ | public boolean isSideLit() { | ||
+ | return true; | ||
+ | } | ||
+ | | ||
+ | // Finally, we can implement the item render function | ||
+ | @Override | ||
+ | public void emitItemQuads(ItemStack itemStack, Supplier< | ||
+ | mesh.outputTo(context.getEmitter()); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Loading the model ==== | ||
+ | Let's update the '' | ||
+ | <code java> | ||
+ | @Environment(EnvType.CLIENT) | ||
+ | public class TutorialModelProvider implements ModelResourceProvider { | ||
+ | public static final FourSidedFurnaceModel FOUR_SIDED_FURNACE_MODEL = new FourSidedFurnaceModel(); | ||
+ | public static final ModelIdentifier FOUR_SIDED_FURNACE_MODEL_BLOCK = new ModelIdentifier(" | ||
+ | public static final ModelIdentifier FOUR_SIDED_FURNACE_MODEL_ITEM = new ModelIdentifier(" | ||
+ | |||
+ | @Override | ||
+ | public UnbakedModel loadModelResource(Identifier identifier, ModelProviderContext modelProviderContext) throws ModelProviderException { | ||
+ | if(identifier.equals(FOUR_SIDED_FURNACE_MODEL_BLOCK) || identifier.equals(FOUR_SIDED_FURNACE_MODEL_ITEM)) { | ||
+ | return FOUR_SIDED_FURNACE_MODEL; | ||
+ | } else { | ||
+ | return null; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ===== Final result ===== | ||
+ | {{: | ||
+ | |||
+ | Et voilà! Enjoy! | ||
+ | |||
+ | ===== More dynamic rendering ===== | ||
+ | The '' | ||
+ | <code java> | ||
+ | @Override | ||
+ | public void emitBlockQuads(BlockRenderView blockRenderView, | ||
+ | QuadEmitter emitter = renderContext.getEmitter(); | ||
+ | /* With this emitter, you can directly append the quads to the chunk model. */ | ||
+ | } | ||
+ | </ |
tutorial/custom_model.1597257184.txt.gz · Last modified: 2020/08/12 18:33 by technici4n