User Tools

Site Tools


tutorial:custom_model

Differences

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

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
tutorial:custom_model [2020/08/13 23:19] – [custom model] Formatting 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. 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.
  
Line 10: Line 10:
  
 <code java> <code java>
 +@Environment(EnvType.CLIENT)
 public class FourSidedFurnaceModel implements UnbakedModel, BakedModel, FabricBakedModel { public class FourSidedFurnaceModel implements UnbakedModel, BakedModel, FabricBakedModel {
 </code> </code>
Line 18: 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("minecraft:block/furnace_front_on")), +            new SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE, new Identifier("minecraft:block/furnace_front_on")), 
-            new SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEX, new Identifier("minecraft:block/furnace_top"))+            new SpriteIdentifier(SpriteAtlasTexture.BLOCK_ATLAS_TEXTURE, new Identifier("minecraft:block/furnace_top"))
     };     };
-    private Sprite[] SPRITES = new Sprite[2];+    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> </code>
  
Line 34: Line 40:
     @Override     @Override
     public Collection<Identifier> getModelDependencies() {     public Collection<Identifier> getModelDependencies() {
-        return Collections.emptyList(); // This model does not depend on other models.+        return List.of(); // This model does not depend on other models.
     }     }
  
     @Override     @Override
-    public Collection<SpriteIdentifier> getTextureDependencies(Function<Identifier, UnbakedModel> unbakedModelGetter, Set<Pair<String, String>> unresolvedTextureReferences) { +    public void setParents(Function<Identifier, UnbakedModel> modelLoader) { 
-        return Arrays.asList(SPRITE_IDS); // The textures this model (and all its model dependenciesand their dependencies, etc...!) depends on.+        // This is related to model parentsit's not required for our use case
     }     }
  
Line 46: Line 52:
     public BakedModel bake(ModelLoader loader, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings rotationContainer, Identifier modelId) {     public BakedModel bake(ModelLoader loader, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings rotationContainer, Identifier modelId) {
         // Get the sprites         // Get the sprites
-        for(int i = 0; i < 2; ++i) { +        for(int i = 0; i < SPRITE_IDS.length; ++i) { 
-            SPRITES[i] = textureGetter.apply(SPRITE_IDS[i]);+            sprites[i] = textureGetter.apply(SPRITE_IDS[i]);
         }         }
         // Build the mesh using the Renderer API         // Build the mesh using the Renderer API
Line 55: Line 61:
  
         for(Direction direction : Direction.values()) {         for(Direction direction : Direction.values()) {
-            int spriteIdx = direction == Direction.UP || direction == Direction.DOWN ? 0;+            // 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             // Add a new face to the mesh
             emitter.square(direction, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f);             emitter.square(direction, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f);
             // Set the sprite of the face, must be called after .square()             // 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.             // We haven't specified any UV coordinates, so we want to use the whole texture. BAKE_LOCK_UV does exactly that.
-            emitter.spriteBake(0, SPRITES[spriteIdx], MutableQuadView.BAKE_LOCK_UV);+            emitter.spriteBake(sprites[spriteIdx], MutableQuadView.BAKE_LOCK_UV);
             // 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             // Add the quad to the mesh
             emitter.emit();             emitter.emit();
Line 73: Line 80:
  
 ==== BakedModel methods ==== ==== BakedModel methods ====
-The methods here are not used by the Fabric Renderer, so we don't really care about the implementation.+Not all the methods here are used by the Fabric Renderer, so we don't really care about the implementation.
 <code java> <code java>
-@Override+    @Override
     public List<BakedQuad> getQuads(BlockState state, Direction face, Random random) {     public List<BakedQuad> getQuads(BlockState state, Direction face, Random random) {
-        return null; // Don't need because we use FabricBakedModel instead+        // 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 false; // Again, we don't really care, etc...+        return true; // we want the block to have a shadow depending on the adjacent blocks
     }     }
  
     @Override     @Override
-    public boolean hasDepth() {+    public boolean isBuiltin() {
         return false;         return false;
     }     }
  
     @Override     @Override
-    public boolean isSideLit() {+    public boolean hasDepth() {
         return false;         return false;
     }     }
  
     @Override     @Override
-    public boolean isBuiltin() {+    public boolean isSideLit() {
         return false;         return false;
     }     }
  
     @Override     @Override
-    public Sprite getSprite() { +    public Sprite getParticleSprite() { 
-        return SPRITES[1]; // Block break particle, let's use furnace_top+        return sprites[SPRITE_TOP]; // Block break particle, let's use furnace_top
     }     }
  
Line 128: Line 136:
                  
         // We just render the mesh         // We just render the mesh
-        renderContext.meshConsumer().accept(mesh);+        mesh.outputTo(context.getEmitter());
     }     }
  
Line 137: Line 145:
 } }
 </code> </code>
 +
 +Note: Make sure you override the ''FabricBakedModel'' methods, the interface has ''default'' implementations!
  
 ===== Registering the model ===== ===== Registering the model =====
-Let's first write a ''ModelResourceProvider'', an interface that allows you to provide an ''UnbakedModel'' before the game tries to load it from JSON. Have a look at [[https://github.com/FabricMC/fabric/blob/1.16/fabric-models-v0/src/main/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java|the documentation]] for more details. The important part is that ''loadModelResource()'' will be called for every 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", "");
  
-Let's register the model under the name ''tutorial:block/four_sided_furnace''. 
-<code java> 
-public class TutorialModelProvider implements ModelResourceProvider { 
-    public static final Identifier FOUR_SIDED_FURNACE_MODEL = new Identifier("tutorial:block/four_sided_furnace"); 
     @Override     @Override
-    public UnbakedModel loadModelResource(Identifier identifier, ModelProviderContext modelProviderContextthrows ModelProviderException +    public void onInitializeModelLoader(Context pluginContext) { 
-        if(identifier.equals(FOUR_SIDED_FURNACE_MODEL)) { +        // We want to add our model when the models are loaded 
-            return new FourSidedFurnaceModel(); +        pluginContext.modifyModelOnLoad().register((original, context) -> { 
-        } else { +            // This is called for every model that is loaded, so make sure we only target ours 
-            return null+            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> </code>
  
-Now we have to register this class in the client initializer, the entry point for client-specific code.+Then you need to register the plugin we just created:
 <code java> <code java>
 +@Environment(EnvType.CLIENT)
 public class ExampleModClient implements ClientModInitializer { public class ExampleModClient implements ClientModInitializer {
     @Override     @Override
     public void onInitializeClient() {     public void onInitializeClient() {
-        ModelLoadingRegistry.INSTANCE.registerResourceProvider(rm -> new TutorialModelProvider()); +        ModelLoadingPlugin.register(new TutorialModelLoadingPlugin()); 
-        + 
         /* Other client-specific initialization */         /* Other client-specific initialization */
     }     }
Line 174: Line 190:
     /* ... */     /* ... */
     "client": [     "client": [
-      "tutorial.path.to.ExampleModClient"+      "net.fabricmc.example.ExampleModClient"
     ]     ]
   },   },
Line 197: Line 213:
  
 ==== Updating the model ==== ==== Updating the model ====
-We will re-use the same model class, with a few changes: +We will re-use the same model class, with just small change
-  * We will register the same model instance under a different name, so we'll make sure the model is only baked once. +  * 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 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 will use the one from "minecraft:block/block" which we will load during model baking.+
  
 We will update our ''FourSidedFurnaceModel'' class as follows: We will update our ''FourSidedFurnaceModel'' class as follows:
 <code java> <code java>
-    // The minecraft default block model 
-    private static final Identifier DEFAULT_BLOCK_MODEL = new Identifier("minecraft:block/block"); 
- 
-    private boolean isBaked = false; 
-    private ModelTransformation transformation; 
-     
-    // We need to add the default model to the dependencies 
-    public Collection<Identifier> getModelDependencies() { 
-        return Arrays.asList(DEFAULT_BLOCK_MODEL); 
-    } 
-     
-    // We need to add a bit of logic to the bake function 
-    @Override 
-    public BakedModel bake(ModelLoader loader, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings rotationContainer, Identifier modelId) { 
-        // Don't bake twice 
-        if(isBaked) return this; 
-        isBaked = true; 
-         
-        // Load the default block model 
-        JsonUnbakedModel defaultBlockModel = (JsonUnbakedModel) loader.getOrLoadModel(DEFAULT_BLOCK_MODEL); 
-        // Get its ModelTransformation 
-        transformation = defaultBlockModel.getTransformations(); 
-         
-        /* Previous code */ 
-    } 
-     
     // We need to implement getTransformation() and getOverrides()     // We need to implement getTransformation() and getOverrides()
     @Override     @Override
     public ModelTransformation getTransformation() {     public ModelTransformation getTransformation() {
-        return transformation;+        return ModelHelper.MODEL_TRANSFORM_BLOCK;
     }     }
  
Line 249: Line 238:
     @Override     @Override
     public void emitItemQuads(ItemStack itemStack, Supplier<Random> supplier, RenderContext renderContext) {     public void emitItemQuads(ItemStack itemStack, Supplier<Random> supplier, RenderContext renderContext) {
-        renderContext.meshConsumer().accept(mesh);+        mesh.outputTo(context.getEmitter());
     }     }
 </code> </code>
Line 256: Line 245:
 Let's update the ''ModelResourceProvider'' we created earlier: Let's update the ''ModelResourceProvider'' we created earlier:
 <code java> <code java>
 +@Environment(EnvType.CLIENT)
 public class TutorialModelProvider implements ModelResourceProvider { public class TutorialModelProvider implements ModelResourceProvider {
     public static final FourSidedFurnaceModel FOUR_SIDED_FURNACE_MODEL = new FourSidedFurnaceModel();     public static final FourSidedFurnaceModel FOUR_SIDED_FURNACE_MODEL = new FourSidedFurnaceModel();
-    public static final Identifier FOUR_SIDED_FURNACE_MODEL_BLOCK = new Identifier("tutorial:block/four_sided_furnace"); +    public static final ModelIdentifier FOUR_SIDED_FURNACE_MODEL_BLOCK = new ModelIdentifier("tutorial", "four_sided_furnace", ""); 
-    public static final Identifier FOUR_SIDED_FURNACE_MODEL_ITEM = new Identifier("tutorial:item/four_sided_furnace");+    public static final ModelIdentifier FOUR_SIDED_FURNACE_MODEL_ITEM = new ModelIdentifier("tutorial", "four_sided_furnace", "inventory");
  
     @Override     @Override
Line 270: Line 260:
     }     }
 } }
-</code java>+</code>
  
 ===== Final result ===== ===== Final result =====
Line 276: Line 266:
  
 Et voilà! Enjoy! 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.1597360771.txt.gz · Last modified: 2020/08/13 23:19 by technici4n