User Tools

Site Tools


tutorial:custom_model

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revision
Previous revision
tutorial:custom_model [2020/08/12 17:27] – created technici4ntutorial:custom_model [2023/12/31 21:40] (current) – [Sprites] Missed `int`s gudenau
Line 1: Line 1:
-====== Creating a custom block model (DRAFT) ======+====== Rendering Blocks and Items Dynamically using a custom Model ====== 
 +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 ''BlockEntityRenderer'': [[tutorial:blockentityrenderers]]. 
 + 
 +===== Creating the model ===== 
 +When a model is first registered in Minecraft, its raw data is contained in an ''UnbakedModel''. This data can include shapes or texture names for example. 
 +Later during the initialization, ''UnbakedModel::bake()'' creates a ''BakedModel'', ready for rendering. For rendering to be as fast as possible, as many operations as possible need to be done during baking. We will also implement ''FabricBakedModel'' to make use of the Fabric Renderer API. 
 +Let's create a single ''FourSidedFurnace'' model that will implement all three interfaces. 
 + 
 +<code java> 
 +@Environment(EnvType.CLIENT) 
 +public class FourSidedFurnaceModel implements UnbakedModel, BakedModel, FabricBakedModel { 
 +</code> 
 + 
 +==== Sprites ==== 
 +A ''Sprite'' is necessary for rendering a texture. We must first create a ''SpriteIdentifier'' and then get the corresponding ''Sprite'' while baking the model. 
 +Here, we will use two furnace textures. They are block textures, so they must be loaded from the block atlas ''SpriteAtlasTexture.BLOCK_ATLAS_TEX''
 +<code java> 
 +    private static final SpriteIdentifier[] SPRITE_IDS = new SpriteIdentifier[]{ 
 +            new SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE, new Identifier("minecraft:block/furnace_front_on")), 
 +            new SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE, new Identifier("minecraft:block/furnace_top")) 
 +    }; 
 +    private final Sprite[] sprites = new Sprite[SPRITE_IDS.length]; 
 + 
 +    // 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; 
 + 
 +</code> 
 + 
 +==== Meshes ==== 
 +A ''Mesh'' is a game shape that is ready for rendering with the Fabric Rendering API. We will add one to our class, and we will build it during model baking. 
 +<code java> 
 +    private Mesh mesh; 
 +</code> 
 + 
 +==== UnbakedModel methods ==== 
 +<code java> 
 +    @Override 
 +    public Collection<Identifier> getModelDependencies() { 
 +        return List.of(); // This model does not depend on other models. 
 +    } 
 + 
 +    @Override 
 +    public void setParents(Function<Identifier, UnbakedModel> modelLoader) { 
 +        // This is related to model parents, it's not required for our use case 
 +    } 
 + 
 + 
 +    @Override 
 +    public BakedModel bake(ModelLoader loader, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings rotationContainer, Identifier modelId) { 
 +        // Get the sprites 
 +        for(int i = 0; i < SPRITE_IDS.length; ++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()) { 
 +            // UP and DOWN share the Y axis 
 +            int spriteIdx = direction == Direction.UP || direction == Direction.DOWN ? SPRITE_TOP : SPRITE_SIDE; 
 +            // Add a new face to the mesh 
 +            emitter.square(direction, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f); 
 +            // Set the sprite of the face, must be called after .square() 
 +            // We haven't specified any UV coordinates, so we want to use the whole texture. BAKE_LOCK_UV does exactly that. 
 +            emitter.spriteBake(sprites[spriteIdx], MutableQuadView.BAKE_LOCK_UV); 
 +            // Enable texture usage 
 +            emitter.color(-1, -1, -1, -1); 
 +            // Add the quad to the mesh 
 +            emitter.emit(); 
 +        } 
 +        mesh = builder.build(); 
 + 
 +        return this; 
 +    } 
 +</code> 
 + 
 +==== 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<BakedQuad> getQuads(BlockState state, Direction face, Random random) { 
 +        // 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 
 +    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[SPRITE_TOP]; // Block break particle, let's use furnace_top 
 +    } 
 + 
 +    @Override 
 +    public ModelTransformation getTransformation() { 
 +        return null; 
 +    } 
 + 
 +    @Override 
 +    public ModelOverrideList getOverrides() { 
 +        return null; 
 +    } 
 +</code> 
 + 
 +==== FabricBakedModel methods ==== 
 +<code java> 
 +    @Override 
 +    public boolean isVanillaAdapter() { 
 +        return false; // False to trigger FabricBakedModel rendering 
 +    } 
 + 
 +    @Override 
 +    public void emitBlockQuads(BlockRenderView blockRenderView, BlockState blockState, BlockPos blockPos, Supplier<Random> supplier, RenderContext renderContext) { 
 +        // Render function 
 +         
 +        // We just render the mesh 
 +        mesh.outputTo(context.getEmitter()); 
 +    } 
 + 
 +    @Override 
 +    public void emitItemQuads(ItemStack itemStack, Supplier<Random> supplier, RenderContext renderContext) { 
 + 
 +    } 
 +
 +</code> 
 + 
 +Note: Make sure you override the ''FabricBakedModel'' methods, the interface has ''default'' implementations! 
 + 
 +===== Registering the model ===== 
 +In order for the model to be rendered in game we need to register it. In order to register it you need to create a ''ModelLoadingPlugin'': 
 +<code java> 
 +@Environment(EnvType.CLIENT) 
 +public class TutorialModelLoadingPlugin impelements ModelLoadingPlugin { 
 +    public static final ModelIdentifier FOUR_SIDED_FURNACE_MODEL = new ModelIdentifier("tutorial", "four_sided_furnace", ""); 
 + 
 +    @Override 
 +    public void onInitializeModelLoader(Context pluginContext) { 
 +        // We want to add our model when the models are loaded 
 +        pluginContext.modifyModelOnLoad().register((original, context) -> { 
 +            // 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 new FourSidedFurnaceModel(); 
 +            } else { 
 +                // If we don't modify the model we just return the original as-is 
 +                return original; 
 +            } 
 +        }); 
 +    } 
 +
 +</code> 
 + 
 +Then you need to register the plugin we just created: 
 +<code java> 
 +@Environment(EnvType.CLIENT) 
 +public class ExampleModClient implements ClientModInitializer { 
 +    @Override 
 +    public void onInitializeClient() { 
 +        ModelLoadingPlugin.register(new TutorialModelLoadingPlugin()); 
 +  
 +        /* Other client-specific initialization */ 
 +    } 
 +
 +</code> 
 + 
 +Don't forget to register this entrypoint in ''fabric.mod.json'' if you haven't done it yet: 
 +<code json> 
 +/* ... */ 
 +  "entrypoints":
 +    /* ... */ 
 +    "client":
 +      "net.fabricmc.example.ExampleModClient" 
 +    ] 
 +  }, 
 +</code> 
 + 
 +===== 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 ''assets/your_mod_id/blockstates/your_block_id.json''
 +<code json> 
 +
 +  "variants":
 +    "": { "model": "tutorial:block/four_sided_furnace"
 +  } 
 +
 +</code> 
 + 
 +Of course, you can implement much more complex rendering. Have fun! 
 + 
 +{{:tutorial:four_sided_furnace_render.png?nolink&600|}} 
 + 
 +===== 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 ''ModelTransformation'' that rotates/translates/scales the model depending on its position (in right hand, in left hand, in gui, in item frame, etc...). As we are creating a model for a regular block, we can use the transform provided by fabric in ''ModelHelper.MODEL_TRANSFORM_BLOCK''
 + 
 +We will update our ''FourSidedFurnaceModel'' class as follows: 
 +<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<Random> supplier, RenderContext renderContext) { 
 +        mesh.outputTo(context.getEmitter()); 
 +    } 
 +</code> 
 + 
 +==== Loading the model ==== 
 +Let's update the ''ModelResourceProvider'' we created earlier: 
 +<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("tutorial", "four_sided_furnace", ""); 
 +    public static final ModelIdentifier FOUR_SIDED_FURNACE_MODEL_ITEM = new ModelIdentifier("tutorial", "four_sided_furnace", "inventory"); 
 + 
 +    @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; 
 +        } 
 +    } 
 +
 +</code> 
 + 
 +===== Final result ===== 
 +{{:tutorial:four_sided_furnace_render_final.png?nolink&600|}} 
 + 
 +Et voilà! Enjoy! 
 + 
 +===== More dynamic rendering ===== 
 +The ''renderContext'' parameter in ''emitBlockQuads'' and ''emitItemQuads'' contains a ''QuadEmitter'' which you can use to build a model on the fly. 
 +<code java> 
 +    @Override 
 +    public void emitBlockQuads(BlockRenderView blockRenderView, BlockState blockState, BlockPos blockPos, Supplier<Random> supplier, RenderContext renderContext) { 
 +        QuadEmitter emitter = renderContext.getEmitter(); 
 +        /* With this emitter, you can directly append the quads to the chunk model. */ 
 +    } 
 +</code>
tutorial/custom_model.1597253279.txt.gz · Last modified: 2020/08/12 17:27 by technici4n