tutorial:custom_model
Differences
This shows you the differences between two versions of the page.
Next revision | Previous revisionNext revisionBoth sides next revision | ||
tutorial:custom_model [2020/08/12 17:27] – created technici4n | tutorial:custom_model [2023/06/25 21:41] – fix typo (EntType -> EnvType) andrew6rant | ||
---|---|---|---|
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. | ||
+ | |||
+ | Note that models are rendered when the chunks are rebuilt. If you need more dynamic rendering, you can use a '' | ||
+ | |||
+ | ===== Creating the model ===== | ||
+ | When a model is first registered in Minecraft, its raw data is contained in an '' | ||
+ | Later during the initialization, | ||
+ | Let's create a single '' | ||
+ | |||
+ | <code java> | ||
+ | @Environment(EnvType.CLIENT) | ||
+ | public class FourSidedFurnaceModel implements UnbakedModel, | ||
+ | </ | ||
+ | |||
+ | ==== Sprites | ||
+ | A '' | ||
+ | Here, we will use two furnace textures. They are block textures, so they must be loaded from the block atlas '' | ||
+ | <code java> | ||
+ | private static final SpriteIdentifier[] SPRITE_IDS = new SpriteIdentifier[]{ | ||
+ | new SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE, | ||
+ | new SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE, | ||
+ | }; | ||
+ | private Sprite[] SPRITES = new Sprite[2]; | ||
+ | </ | ||
+ | |||
+ | ==== Meshes ==== | ||
+ | A '' | ||
+ | <code java> | ||
+ | private Mesh mesh; | ||
+ | </ | ||
+ | |||
+ | ==== UnbakedModel methods ==== | ||
+ | <code java> | ||
+ | @Override | ||
+ | public Collection< | ||
+ | return Collections.emptyList(); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public Collection< | ||
+ | return Arrays.asList(SPRITE_IDS); | ||
+ | } | ||
+ | |||
+ | |||
+ | @Override | ||
+ | public BakedModel bake(ModelLoader loader, Function< | ||
+ | // Get the sprites | ||
+ | for(int i = 0; i < 2; ++i) { | ||
+ | SPRITES[i] = textureGetter.apply(SPRITE_IDS[i]); | ||
+ | } | ||
+ | // Build the mesh using the Renderer API | ||
+ | Renderer renderer = RendererAccess.INSTANCE.getRenderer(); | ||
+ | MeshBuilder builder = renderer.meshBuilder(); | ||
+ | QuadEmitter emitter = builder.getEmitter(); | ||
+ | |||
+ | for(Direction direction : Direction.values()) { | ||
+ | int spriteIdx = direction == Direction.UP || direction == Direction.DOWN ? 1 : 0; | ||
+ | // Add a new face to the mesh | ||
+ | emitter.square(direction, | ||
+ | // Set the sprite of the face, must be called after .square() | ||
+ | // We haven' | ||
+ | emitter.spriteBake(0, | ||
+ | // Enable texture usage | ||
+ | emitter.spriteColor(0, | ||
+ | // Add the quad to the mesh | ||
+ | emitter.emit(); | ||
+ | } | ||
+ | mesh = builder.build(); | ||
+ | |||
+ | return this; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Note that the type parameter "'' | ||
+ | |||
+ | ==== BakedModel methods ==== | ||
+ | Not all the methods here are used by the Fabric Renderer, so we don't really care about the implementation. | ||
+ | <code java> | ||
+ | @Override | ||
+ | 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 Collections.emptyList(); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public boolean useAmbientOcclusion() { | ||
+ | return true; // we want the block to have a shadow depending on the adjacent blocks | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public boolean isBuiltin() { | ||
+ | return false; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public boolean hasDepth() { | ||
+ | return false; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public boolean isSideLit() { | ||
+ | return false; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public Sprite getParticleSprite() { | ||
+ | return SPRITES[1]; // Block break particle, let's use furnace_top | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public ModelTransformation getTransformation() { | ||
+ | return null; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public ModelOverrideList getOverrides() { | ||
+ | return null; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== FabricBakedModel methods ==== | ||
+ | <code java> | ||
+ | @Override | ||
+ | public boolean isVanillaAdapter() { | ||
+ | return false; // False to trigger FabricBakedModel rendering | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public void emitBlockQuads(BlockRenderView blockRenderView, | ||
+ | // Render function | ||
+ | |||
+ | // We just render the mesh | ||
+ | renderContext.meshConsumer().accept(mesh); | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public void emitItemQuads(ItemStack itemStack, Supplier< | ||
+ | |||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ===== Registering the model ===== | ||
+ | Let's first write a '' | ||
+ | |||
+ | Let's register the model under the name '' | ||
+ | <code java> | ||
+ | @Environment(EnvType.CLIENT) | ||
+ | public class TutorialModelProvider implements ModelResourceProvider { | ||
+ | public static final Identifier FOUR_SIDED_FURNACE_MODEL = new Identifier(" | ||
+ | @Override | ||
+ | public UnbakedModel loadModelResource(Identifier identifier, ModelProviderContext modelProviderContext) throws ModelProviderException { | ||
+ | if(identifier.equals(FOUR_SIDED_FURNACE_MODEL)) { | ||
+ | return new FourSidedFurnaceModel(); | ||
+ | } else { | ||
+ | return null; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Now we have to register this class in the client initializer, | ||
+ | <code java> | ||
+ | @Environment(EnvType.CLIENT) | ||
+ | public class ExampleModClient implements ClientModInitializer { | ||
+ | @Override | ||
+ | public void onInitializeClient() { | ||
+ | ModelLoadingRegistry.INSTANCE.registerResourceProvider(rm -> new TutorialModelProvider()); | ||
+ | |||
+ | /* Other client-specific initialization */ | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Don't forget to register this entrypoint in '' | ||
+ | <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> | ||
+ | { | ||
+ | " | ||
+ | "": | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Of course, you can implement much more complex rendering. Have fun! | ||
+ | |||
+ | {{: | ||
+ | |||
+ | ===== 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< | ||
+ | renderContext.meshConsumer().accept(mesh); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== 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 Identifier FOUR_SIDED_FURNACE_MODEL_BLOCK = new Identifier(" | ||
+ | public static final Identifier FOUR_SIDED_FURNACE_MODEL_ITEM = new Identifier(" | ||
+ | |||
+ | @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.txt · Last modified: 2024/04/27 08:58 by florens