Skip to content

Latest commit

 

History

History
698 lines (584 loc) · 23.8 KB

blockModel.md

File metadata and controls

698 lines (584 loc) · 23.8 KB

BlockModel物品模型


JSON Model Structure

方块的物品模型,与json模型相比更加之复杂,我们会阐释一下

首先是models/block/block.json,定义了方块在不同视角/TransformType对应的变换参数

block.json
{
    "gui_light": "side",
    "display": {
        "gui": {
            "rotation": [ 30, 225, 0 ],
            "translation": [ 0, 0, 0],
            "scale":[ 0.625, 0.625, 0.625 ]
        },
        "ground": {
            "rotation": [ 0, 0, 0 ],
            "translation": [ 0, 3, 0],
            "scale":[ 0.25, 0.25, 0.25 ]
        },
        "fixed": {
            "rotation": [ 0, 0, 0 ],
            "translation": [ 0, 0, 0],
            "scale":[ 0.5, 0.5, 0.5 ]
        },
        "thirdperson_righthand": {
            "rotation": [ 75, 45, 0 ],
            "translation": [ 0, 2.5, 0],
            "scale": [ 0.375, 0.375, 0.375 ]
        },
        "firstperson_righthand": {
            "rotation": [ 0, 45, 0 ],
            "translation": [ 0, 0, 0 ],
            "scale": [ 0.40, 0.40, 0.40 ]
        },
        "firstperson_lefthand": {
            "rotation": [ 0, 225, 0 ],
            "translation": [ 0, 0, 0 ],
            "scale": [ 0.40, 0.40, 0.40 ]
        }
    }
}

然后对于普通的六面方块来说,都来直接或间接来自models/block/cube.json

{
    "parent": "block/block",
    "elements": [
        {   "from": [ 0, 0, 0 ],
            "to": [ 16, 16, 16 ],
            "faces": {
                "down":  { "texture": "#down", "cullface": "down" },
                "up":    { "texture": "#up", "cullface": "up" },
                "north": { "texture": "#north", "cullface": "north" },
                "south": { "texture": "#south", "cullface": "south" },
                "west":  { "texture": "#west", "cullface": "west" },
                "east":  { "texture": "#east", "cullface": "east" }
            }
        }
    ]
}

这里比较特殊的是texture后面的#down #up等,将会查找自将继承此模型的模型,比如models/block/cube_all.json

{
    "parent": "block/cube",
    "textures": {
        "particle": "#all",
        "down": "#all",
        "up": "#all",
        "north": "#all",
        "east": "#all",
        "south": "#all",
        "west": "#all"
    }
}

其他非普通六面的模型,则定义其elements,如台阶

stair.json
{   "parent": "block/block",
    "display": {
        "gui": {
            "rotation": [ 30, 135, 0 ],
            "translation": [ 0, 0, 0],
            "scale":[ 0.625, 0.625, 0.625 ]
        },
        "head": {
            "rotation": [ 0, -90, 0 ],
            "translation": [ 0, 0, 0 ],
            "scale": [ 1, 1, 1 ]
        },
        "thirdperson_lefthand": {
            "rotation": [ 75, -135, 0 ],
            "translation": [ 0, 2.5, 0],
            "scale": [ 0.375, 0.375, 0.375 ]
        }
    },
    "textures": {
        "particle": "#side"
    },
    "elements": [
        {   "from": [ 0, 0, 0 ],
            "to": [ 16, 8, 16 ],
            "faces": {
                "down":  { "uv": [ 0, 0, 16, 16 ], "texture": "#bottom", "cullface": "down" },
                "up":    { "uv": [ 0, 0, 16, 16 ], "texture": "#top" },
                "north": { "uv": [ 0, 8, 16, 16 ], "texture": "#side", "cullface": "north" },
                "south": { "uv": [ 0, 8, 16, 16 ], "texture": "#side", "cullface": "south" },
                "west":  { "uv": [ 0, 8, 16, 16 ], "texture": "#side", "cullface": "west" },
                "east":  { "uv": [ 0, 8, 16, 16 ], "texture": "#side", "cullface": "east" }
            }
        },
        {   "from": [ 8, 8, 0 ],
            "to": [ 16, 16, 16 ],
            "faces": {
                "up":    { "uv": [ 8, 0, 16, 16 ], "texture": "#top", "cullface": "up" },
                "north": { "uv": [ 0, 0,  8,  8 ], "texture": "#side", "cullface": "north" },
                "south": { "uv": [ 8, 0, 16,  8 ], "texture": "#side", "cullface": "south" },
                "west":  { "uv": [ 0, 0, 16,  8 ], "texture": "#side" },
                "east":  { "uv": [ 0, 0, 16,  8 ], "texture": "#side", "cullface": "east" }
            }
        }
    ]
}

可以观察出,一个element,由fromto定义其在16个体素范围内的位置,由face定义其每个面的材质

Note

方块对应的物品默认是没有材质的
请详见ModelResourceLocationUse Block Model

BlockState

打开游戏,按下f3,可以看到右侧的Targeted BlockTargeted Fluid下,除了显示所指向的方块名称 在以#为标识的Tag之上,有的方块/流体还显示了一些别的信息,这就是BlockState

在Minecraft内创建世界的时候,有一种隐藏的世界类型称之为debug mode,见wiki
里面枚举所有方块/流体的BlockState

想要为你的方块添加BlockState,你需要复写protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> pBuilder)
原版已定义的在类BlockStateProperties内,可以直接引用

若你想创建自己的BlockState,可以选择实现抽象类net.minecraft.world.level.block.state.properties.Property>
当然,原版已经有特化实现BooleanProperty,DirectionProperty,EnumProperty,IntegerProperty

Warning

方块所持有的BlockState会在加载模型时候,穷举其所有排列组合,即笛卡尔积
请确保其枚举总可能结果数量在一个合理的范围内

BlockState的切换需要你手动设置,可以覆写诸如
getStateForPlacement在放置时设置
neighborChanged毗邻方块更新时设置
updateIndirectNeighbourShapes,updateShape

JSON model


Variants Model

原版拥有两种以BlockState描述模型的方式,在wiki都有描述 其一便是Variants Block Model
其思路为排列组合枚举所有的BlockState,并要求逐一给出对应的模型
这里以草方块为例,其拥有一个布尔类型的名为snow的BlockState

首先是blockState的文件,blockstates/grass_block.json

{
  "variants": {
    "snowy=false": [
      {
        "model": "minecraft:block/grass_block"
      },
      {
        "model": "minecraft:block/grass_block",
        "y": 90
      },
      {
        "model": "minecraft:block/grass_block",
        "y": 180
      },
      {
        "model": "minecraft:block/grass_block",
        "y": 270
      }
    ],
    "snowy=true": {
      "model": "minecraft:block/grass_block_snow"
    }
  }
}

然后是模型文件

block/grass_block.json

{   "parent": "block/block",
  "textures": {
    "particle": "block/dirt",
    "bottom": "block/dirt",
    "top": "block/grass_block_top",
    "side": "block/grass_block_side",
    "overlay": "block/grass_block_side_overlay"
  },
  "elements": [
    {   "from": [ 0, 0, 0 ],
      "to": [ 16, 16, 16 ],
      "faces": {
        "down":  { "uv": [ 0, 0, 16, 16 ], "texture": "#bottom", "cullface": "down" },
        "up":    { "uv": [ 0, 0, 16, 16 ], "texture": "#top",    "cullface": "up", "tintindex": 0 },
        "north": { "uv": [ 0, 0, 16, 16 ], "texture": "#side",   "cullface": "north" },
        "south": { "uv": [ 0, 0, 16, 16 ], "texture": "#side",   "cullface": "south" },
        "west":  { "uv": [ 0, 0, 16, 16 ], "texture": "#side",   "cullface": "west" },
        "east":  { "uv": [ 0, 0, 16, 16 ], "texture": "#side",   "cullface": "east" }
      }
    },
    {   "from": [ 0, 0, 0 ],
      "to": [ 16, 16, 16 ],
      "faces": {
        "north": { "uv": [ 0, 0, 16, 16 ], "texture": "#overlay", "tintindex": 0, "cullface": "north" },
        "south": { "uv": [ 0, 0, 16, 16 ], "texture": "#overlay", "tintindex": 0, "cullface": "south" },
        "west":  { "uv": [ 0, 0, 16, 16 ], "texture": "#overlay", "tintindex": 0, "cullface": "west" },
        "east":  { "uv": [ 0, 0, 16, 16 ], "texture": "#overlay", "tintindex": 0, "cullface": "east" }
      }
    }
  ]
}

block/grass_block_snow

{
  "parent": "minecraft:block/cube_bottom_top",
  "textures": {
    "top": "minecraft:block/grass_block_top",
    "bottom": "minecraft:block/dirt",
    "side": "minecraft:block/grass_block_snow",
    "particle": "minecraft:block/dirt"
  }
}

item/grass_block

{
  "parent": "minecraft:block/grass_block"
}

这里我们给出一个多阶段作物的例子

exampleForVariant

Multipart Model

另一种则称之为Multipart Model,与Variants不同,这种方式可以视为模型在一系列条件下的叠加
以原版的栅栏为例

{
  "multipart": [
    {
      "apply": {"model": "minecraft:block/acacia_fence_post"}
    },
    {
      "when": {"north": "true"},
      "apply": {"model": "minecraft:block/acacia_fence_side","uvlock": true}
    },
    {
      "when": {"east": "true"},
      "apply": {"model": "minecraft:block/acacia_fence_side","y": 90,"uvlock": true}
    },
    {
      "when": {"south": "true"},
      "apply": {"model": "minecraft:block/acacia_fence_side","y": 180,"uvlock": true}
    },
    {
      "when": {"west": "true"},
      "apply": {"model": "minecraft:block/acacia_fence_side","y": 270,"uvlock": true}
    }
  ]
}

其渲染流程可视为,对一系列when进行判断,如果成立,则叠加/应用/apply所指定的模型
当然这这一系列操作并不会发生在渲染时,在模型加载阶段就已经完成

在这里我们给出一个管道的例子

exampleForMultiPart

RenderType

参见renderType

如果你的方块使用了带有alpha通道的贴图,那么你可能需要调用ItemBlockRenderTypes#setRenderLayer

noOcclusion

如果你的方块出现了不正常的面剔除现象,那么你需要为你的方块的BlockBehaviour.Properties#noOcclusion 差别可见图片上下两排

IModelData

forge对于原版的扩充,基本可以理解为一个Map<ModelProperty<T>,T>
原版未使用的地方,forge大多进行了wrapper,并传入一个EmptyModelData.INSTANCE

public interface IModelData
{
    /**
     * Check if this data has a property, even if the value is {@code null}. Can be
     * used by code that intends to fill in data for a render pipeline, such as the
     * forge animation system.
     * <p>
     * IMPORTANT: {@link #getData(ModelProperty)} <em>can</em> return {@code null}
     * even if this method returns {@code true}.
     * 
     * @param prop The property to check for inclusion in this model data
     * @return {@code true} if this data has the given property, even if no value is present
     */
    boolean hasProperty(ModelProperty<?> prop);

    @Nullable
    <T> T getData(ModelProperty<T> prop);
    
    @Nullable
    <T> T setData(ModelProperty<T> prop, T data);
}

CTM这种链接纹理,依靠的就是这个机制

和他一样很重要的是ModelDataManager,可以理解为带拥有缓存,刷新等机制的Map<BlockPos,IModelData>
这是配合BlockEntity使用的

forge通过IForgeBlockEntityBlockEntiy添加了
requestModelDataUpdate()
default @Nonnull IModelData getModelData()

Coloring

与物品一样,方块也可以染色,原理也一致,只不过接口变了而已

@OnlyIn(Dist.CLIENT)
public interface BlockColor {
   int getColor(BlockState pState, @Nullable BlockAndTintGetter pLevel, @Nullable BlockPos pPos, int pTintIndex);
}

但是方块与物品不同,不存在所谓的BlockStack,BlockState也无法支持任意颜色的方块存在
因此,我们需要另外的载体,来存储方块所需的数据

example

给出的例子是,利用上一章制作的Colorful Chalk内的数据,在其右键我们的Colorful Block的时候
设置它所拥有的BlockEntity内的字段
当我们的方块所对应的getColor被调用时,从BlockEntity中获得颜色信息

ColorfulBlockByBlockEntity

class ColorfulBlockByBlockEntity : Block(Properties.of(Material.STONE)), EntityBlock {

    companion object {
        @JvmStatic
        fun registerColorHandle(event: ColorHandlerEvent.Block) {
            event.blockColors.register(
                { pState, pLevel, pPos, pTintIndex ->
                    if (pLevel != null && pPos != null) {
                        val blockEntity = pLevel.getBlockEntity(pPos) as? ColorfulBlockEntity
                        //当方块被破坏后,由于需要渲染方块被破坏的粒子,此处会被调用  
                        //但是由于坐标所处的`BlockEntity`已经无法获取,所以会出错,只能使用`as?`
                        return@register blockEntity?.color ?: 0xffffff
                    }
                    0xffffff
                }, AllRegisters.colorfulBlockByBlockEntity.get()
            )
        }
    }

    override fun newBlockEntity(pPos: BlockPos, pState: BlockState): BlockEntity = ColorfulBlockEntity(pPos, pState)

    override fun use(
        pState: BlockState,
        pLevel: Level,
        pPos: BlockPos,
        pPlayer: Player,
        pHand: InteractionHand,
        pHit: BlockHitResult
    ): InteractionResult {
        if (pHand != InteractionHand.MAIN_HAND) {
            return super.use(pState, pLevel, pPos, pPlayer, pHand, pHit)
        }
        val itemStack = pPlayer.getItemInHand(pHand)
        val item = itemStack.item as? ColorfulChalk
        val blockEntity = pLevel.getBlockEntity(pPos) as ColorfulBlockEntity
        if (item != null) {
            if (!pLevel.isClientSide) {
                val color = item.getColor(itemStack)
                blockEntity.color = color
                return InteractionResult.SUCCESS
            }
        } else {
            val color = Integer.toHexString(blockEntity.color)
            if (pLevel.isClientSide) {
                pPlayer.sendMessage(TextComponent("client:entity color:$color"), Util.NIL_UUID)
            } else {
                pPlayer.sendMessage(TextComponent("server:entity color:$color"), Util.NIL_UUID)
            }
        }
        return super.use(pState, pLevel, pPos, pPlayer, pHand, pHit)
    }
}
public class ColorfulBlockByBlockEntity extends Block implements EntityBlock {

	public ColorfulBlockByBlockEntity() {
		super(new Properties.of(Material.STONE));
	}

    public static void registerColorHandle(ColorHandlerEvent.Block event) {
        event.blockColors.register((pState, pLevel, pPos, pTintIndex) -> {
                if (pLevel != null && pPos != null) {
                    final var blockEntity = ((ColorfulBlockEntity)(pLevel)).getBlockEntity(pPos);
                    //当方块被破坏后,由于需要渲染方块被破坏的粒子,此处会被调用  
                    //但是由于坐标所处的`BlockEntity`已经无法获取,所以会出错,需要额外判断
                    if(blockEntity != null){
                        return blockEntity.color;
                    }
                }
                return 0xffffff;
            }, AllRegisters.colorfulBlockByBlockEntity.get()
        );
    }

	@Override
    public BlockEntity newBlockEntity(BlockPos pPos,BlockState pState) { 
        return new ColorfulBlockEntity(pPos, pState);
    }
    
	@Override
    public InteractionResult use(BlockState pState,Level pLevel,BlockPos pPos,Player pPlayer,InteractionHand pHand,BlockHitResult pHit){
        if (pHand != InteractionHand.MAIN_HAND) {
            return super.use(pState, pLevel, pPos, pPlayer, pHand, pHit);
        }
        final var itemStack = pPlayer.getItemInHand(pHand);
        if(itemStack.item instanceof ColorfulChalk item) {
	        final var blockEntity = ((ColorfulBlockEntity)(pLevel)).getBlockEntity(pPos);
	        if (item != null) {
	            if (!pLevel.isClientSide) {
	                final var color = item.getColor(itemStack);
	                blockEntity.color = color;
	                return InteractionResult.SUCCESS;
	            }
	        } else {
	            final var color = Integer.toHexString(blockEntity.color);
	            if (pLevel.isClientSide) {
	                pPlayer.sendMessage(TextComponent("client:entity color:$color"), Util.NIL_UUID);
	            } else {
	                pPlayer.sendMessage(TextComponent("server:entity color:$color"), Util.NIL_UUID);
	            }
	        }        
        }
        return super.use(pState, pLevel, pPos, pPlayer, pHand, pHit);
    }
}

ColorfulBlockEntity

class ColorfulBlockEntity(pos: BlockPos, state: BlockState) :
    BlockEntity(AllRegisters.colorfulBlockEntityType.get(), pos, state) {
    var color: Int = 0xffffff
        set(value) = if (value in 0..0xffffff) {
            if (value != field) {
                field = value
                if (level?.isClientSide == true) {
                    Minecraft.getInstance().levelRenderer.setBlocksDirty(
                        worldPosition.x, worldPosition.y, worldPosition.z, worldPosition.x, worldPosition.y, worldPosition.z
                    )
                } else {
                    level?.sendBlockUpdated(worldPosition, blockState, blockState, 1)
                }
                Unit
            } else {
            }
        } else {
            throw AssertionError("color:${Integer.toHexString(value)} not range in 0 to 0xffffff")
        }

    override fun getUpdatePacket(): Packet<ClientGamePacketListener>? =
        ClientboundBlockEntityDataPacket.create(this)

    override fun getUpdateTag(): CompoundTag = CompoundTag().apply { putInt("color", color) }

    override fun handleUpdateTag(tag: CompoundTag?) {
        tag?.getInt("color")?.apply { color = this }
    }

    override fun onDataPacket(net: Connection?, pkt: ClientboundBlockEntityDataPacket?) {
        pkt?.tag?.getInt("color")?.apply { color = this }
    }
}
class ColorfulBlockEntity extends BlockEntity{
    
    public ColorfulBlockEntity(BlockPos pos, BlockState state){
        super(AllRegisters.colorfulBlockEntityType.get(), pos, state);
    }
        
    private int color = 0xffffff;
    
    public int getColor() {
        return color;
    }
    
    public void setColor(int color) {
        if(color < 0 && color > 0xffffff)
            throw new AssertionError("color:" + Integer.toHexString(value) + "} not range in 0 to 0xffffff");
        if(this.color != color) {
            this.color = color;
            if(level == null) {
                return;
            }
            if(level.isClientSide) {
                 Minecraft.getInstance().levelRenderer.setBlocksDirty(
                    worldPosition.x, worldPosition.y, worldPosition.z, worldPosition.x, worldPosition.y, worldPosition.z
                 );                
            }else {
                level.sendBlockUpdated(worldPosition, blockState, blockState, 1);
            }
        }
    }

	@Override
    public Packet<ClientGamePacketListener> getUpdatePacket() {
        return ClientboundBlockEntityDataPacket.create(this);
	}
	
	@Override
	public CompoundTag getUpdateTag() {
		final var tag = CompoundTag();
		tag.putInt("color",color);
		return tag;
	}
	
	@Override
	public void handleUpdateTag(CompoundTag tag) {
		this.color = tag.getInt("color"); //可能需要考虑tag不存在的情况
	}

	@Override
	public void onDataPacket(Connection net, ClientboundBlockEntityDataPacket pkt){
		final var color = pkt.tag.getInt("color"); //两个参数都可能为空
		this.color = color;
	}	
}

Register

private val BLOCK = DeferredRegister.create(ForgeRegistries.BLOCKS, Cobalt.MOD_ID)
private val BLOCKENTITY_TYPE = DeferredRegister.create(ForgeRegistries.BLOCK_ENTITIES, Cobalt.MOD_ID)

val colorfulBlockByBlockEntity = BLOCK.register("colorful_chalk_by_block_entity") { ColorfulBlockByBlockEntity() }

val colorfulBlockByBlockEntityItem = ITEM.register("colorful_chalk_by_block_entity") {
    BlockItem(colorfulBlockByBlockEntity.get(), Item.Properties().tab(creativeTab))
}

val colorfulBlockEntityType = BLOCKENTITY_TYPE.register("colorful_block") {
    BlockEntityType.Builder.of({ pos, state ->
        ColorfulBlockEntity(pos, state)
    }, colorfulBlockByBlockEntity.get()).build(Util.fetchChoiceType(References.BLOCK_ENTITY, "colorful_block"))
}
DeferredRegister<Block> BLOCK = DeferredRegister.create(ForgeRegistries.BLOCKS, Cobalt.MOD_ID);
DeferredRegister<BlockEntityType<?>> = DeferredRegister.create(ForgeRegistries.BLOCK_ENTITIES, Cobalt.MOD_ID);

RegistryObject<ColorfulBlockByBlockEntity> colorfulBlockByBlockEntity 
	= BLOCK.register("colorful_chalk_by_block_entity",ColorfulBlockByBlockEntity::new);

RegistryObject<BlockItem> colorfulBlockByBlockEntityItem = ITEM.register("colorful_chalk_by_block_entity",
    () -> new BlockItem(colorfulBlockByBlockEntity.get(), new Item.Properties().tab(creativeTab))
);

RegistryObject<BlockEntityType<ColorfulBlockByBlockEntity>> colorfulBlockEntityType 
	= BLOCKENTITY_TYPE.register("colorful_block") , () -> BlockEntityType.Builder.of(ColorfulBlockEntity::new,
      colorfulBlockByBlockEntity.get()).build(Util.fetchChoiceType(References.BLOCK_ENTITY, "colorful_block")));

Block Json Model

{
  "parent": "block/block",
  "textures": {
    "down":"minecraft:block/iron_block",
    "up":"minecraft:block/iron_block",
    "north":"minecraft:block/iron_block",
    "south":"minecraft:block/iron_block",
    "west":"minecraft:block/iron_block",
    "east":"minecraft:block/iron_block"
  },
  "elements": [
    {   "from": [ 0, 0, 0 ],
      "to": [ 16, 16, 16 ],
      "faces": {
        "down":  { "texture": "#down", "cullface": "down" ,"tintindex": 0 },
        "up":    { "texture": "#up", "cullface": "up" ,"tintindex": 0 },
        "north": { "texture": "#north", "cullface": "north" ,"tintindex": 0 },
        "south": { "texture": "#south", "cullface": "south" ,"tintindex": 0 },
        "west":  { "texture": "#west", "cullface": "west" ,"tintindex": 0 },
        "east":  { "texture": "#east", "cullface": "east","tintindex": 0  }
      }
    }
  ]
}

Note

这里的JSON模型相当于原版的贴方块
一定要使得**tintindex**不为默认值-1

Warning

注意我们调用的LevelRender#setBlocksDirty
否则方块的数据不会**刷新**
会被阻拦在LevelRender#compileChunks内的ChunkRenderDispatcher.RenderChunk#isDirty
详见RenderChunk的Cache问题

效果如下 colorfulBlock

BlockEntityRender

游戏中,总有那么些东西,看上去不像是普通的模型能过做到的,比如附魔台,那本书的动画,各个mod的机器的动画,透明箱子渲染的其拥有的物品
这一般是用BlockEntityRender实现的

在实现BlockEntityRender#render前,我们需要一系列操作 BlockEntityRender需要BlockEntity配合使用

注册方块,方块需要实现EntityBlock接口
实现抽象方法newBlockEntity,返回BlockEntiy实例
需要注册BlockEntityType,并与你的方块进行绑定
订阅事件EntityRenderersEvent.RegisterRenderers,调用事件内registerBlockEntityRenderer进行绑定

Note

为什么我渲染出的物品都黑漆漆的?
查看你的pPackedLight参数,如果一直是0,可以通过给方块添加noOcclusion或者非完整VoxelShape解决
如做修改,请尝试更新光照,敲掉重放或者放一个光源都可以

而最为重要的render函数的实现,则会在单独的一章中单独介绍