概述
Minecraft原版提供的json
在一定程度上描述方块模型是非常方便的,尤其是当方块模型只有几个长方体的时候,这也符合Minecraft的风格。如果读者有体验过沉浸工程模组(Immersive Engineering)的话,可能会被它华丽的模型大大地震撼到。如果Minecraft原版提供的json
用于描述那些华丽的模型的话,就有些力不从心了。
在这一部分,我们从Forge为了方便渲染以添加的扩展BlockState(ExtendedBlockState
)说起,描述其如何应用于Forge支持的两种第三方模型格式——B3D模型和OBJ模型,并着重讲解使用较为广泛的OBJ模型,包括贴图的选取,与方块模型的仿射变换(包含平移、旋转、放缩等变换及其组合的统称)。
扩展BlockState
我们先从渲染相关的属性的一般角度说起:
- 首先,渲染相关的属性是动态生成的,比如用于渲染的水的四个角的高度,是通过周围的环境实时计算得到的
- 其次,渲染相关的属性是非实时的,一般情况下不会出现每个tick都变化一次的情况
- 最后,渲染相关的属性是连续非可数的,比如水的四个角的高度是一个浮点数据,可能的数量多到可以当做无限
Forge为了解决这些问题,为我们提供了扩展BlockState系统,也就是ExtendedBlockState
,以替代原版的BlockState的方式用作渲染的相关操作。
首先,我们从ExtendedBlockState
类的构造方法的声明看起:
public ExtendedBlockState(Block blockIn, IProperty[] properties, IUnlistedProperty<?>[] unlistedProperties) {...}
第一个参数表示对应的方块,第二个参数表示原版BlockState中的IProperty
,不过这里没有办法使用变长数组了,第三个参数就是ExtendedBlockState
中的和BlockState的IProperty
对应的东西,IUnlistedProperty
。
那么这里我们首先需要一个IUnlistedProperty
。一个比较常用的IUnlistedProperty
是名为PropertyFloat
类的实例,不过用做B3D和OBJ模型上时,Forge为我们提供的就是:B3DLoader.B3DFrameProperty.instance
和OBJModel.OBJProperty.instance
两个。
我们仍然使用Block
类的createBlockState
方法,然而这里我们把其中的BlockState
类,换成ExtendedBlockState
类,在第三个参数传入教程做为演示需要的OBJ模型对应的IUnlistedProperty
:
src/main/java/com/github/ustc_zzzz/fmltutor/block/BlockMetalFurnace.java(部分):
@Override
protected BlockState createBlockState()
{
return new ExtendedBlockState(this, new IProperty<?>[]
{ FACING, BURNING, MATERIAL }, new IUnlistedProperty<?>[]
{ OBJModel.OBJProperty.instance });
}
此外,存储一种BlockState的IBlockState
接口,目前也被换成了IExtendedBlockState
。IExtendedBlockState
接口的使用方法和IBlockState
接口一样,只是之前使用IProperty
的地方,被换成了IUnlistedProperty
而已。
之前我们曾经说过,世界中用于存储BlockState数据的每个方块只有4bit的空间,也就是说,根本没有地方再为扩展BlockState提供存储空间,所以扩展BlockState的获取是实时生成的,这个用于实时生成的方法就是Block
类的getExtendedState
:
src/main/java/com/github/ustc_zzzz/fmltutor/block/BlockMetalFurnace.java(部分):
@Override
public IBlockState getExtendedState(IBlockState state, IBlockAccess world, BlockPos pos)
{
IExtendedBlockState oldState = (IExtendedBlockState) state;
// TODO
return oldState;
}
一种常见的实时获取扩展BlockState的方式是通过TileEntity得到数据,进一步设置扩展BlockState,在本节后面的部分我们也会这么做。
第三方模型格式
Forge提供了B3D和OBJ两种模型的支持,其中对于OBJ模型的支持尤为丰富,不过对于这两种模型的支持,都是通过B3DProperty
和OBJProperty
两种IUnlistedProperty
实现的,Minecraft的风格是方形的,这里作者反其道而行之,使用OBJ模型制作了一个圆柱体,模型文件见这里。作者在本部分不会讲述如何制作OBJ模型,一些诸如Blender的工具,可以很方便地导入导出模型。然后,我们把模型放置在assets.fmltutor.models.block
文件夹里,并自行起名,就像过去把json
类型的方块模型放入同一个文件夹一样。教程作为演示,将其起名为metal_furnace.obj
。
那么如何引用这个模型呢?其实很简单,我们只需要把blockstates
文件夹里描述BlockState的文件中的模型文件名称,换成我们刚刚设置的名称就可以了(这里自然就是metal_furnace.obj
,注意有名为.obj
的后缀):
src/main/resources/assets/fmltutor/blockstates/gold_furnace.json:
{
"forge_marker": 1,
"defaults": {
"model": "fmltutor:metal_furnace.obj"
},
"variants": {
"inventory": [{
}],
"burning": {
"true": {},
"false": {}
}
}
}
src/main/resources/assets/fmltutor/blockstates/iron_furnace.json:
{
"forge_marker": 1,
"defaults": {
"model": "fmltutor:metal_furnace.obj"
},
"variants": {
"inventory": [{
}],
"burning": {
"true": {},
"false": {}
}
}
}
这里因为材质已经不是先前我们想要的了,所以有关的代码全部删掉了。
这里有一个很重要的点,我们使用的OBJ模型必须是被Forge而不是原版的机制所读取的,所以这句话是必要的:
"forge_marker": 1,
这里补充一点,后面我们会讲到如何旋转这个方块,所以正如上一部分所言,名为facing
的属性被作者去掉了,当然,我们还需要在BlockStateMapper
的注册中声明这一点(ignore
方法)。
此外,如果我们想要使用OBJ模型,我们还需要在客户端的preInit
阶段调用OBJLoader.instance.addDomain
方法,并传入Mod id(B3D模型自然是B3DLoader.instance.addDomain
方法),所以归纳起来,我们看到的代码是这样子的:
src/main/java/com/github/ustc_zzzz/fmltutor/block/BlockLoader.java(部分):
@SideOnly(Side.CLIENT)
public static void registerRenders()
{
OBJLoader.instance.addDomain(FMLTutor.MODID);
registerStateMapper(metalFurnace, new StateMap.Builder().withName(BlockMetalFurnace.MATERIAL)
.withSuffix("_furnace").ignore(BlockMetalFurnace.FACING).build());
registerRender(grassBlock);
registerRender(metalFurnace, 0, "iron_furnace");
registerRender(metalFurnace, 8, "gold_furnace");
}
现在我们处理一下OBJ模型的内容,使用文本编辑器打开你制作的OBJ,找到以mtllib
开头的行,在本部分提供的OBJ模型里是这一行:
mtllib metal_furnace.mtl
在空格后的那一项前面加上models/block/
,也就是这个样子:
mtllib models/block/metal_furnace.mtl
如果没有含有mtllib
的那一行,就自己加上这一句,这一句的作用是引用一个外部的后缀为.mtl
的,用于声明材质的文件。
然后我们找到以usemtl
开头的那些行,本部分提供的OBJ模型只有一行,不过很多OBJ模型有很多行:
usemtl cylinder
然后我们打开同文件夹下刚刚使用mtllib
声明的名为metal_furnace.mtl
的文件(如果没有就创建一个),把所有usemtl
开头的行复制下来,换成newmtl
,换言之,我们要保证每一个.obj
文件中的usemtl
,都应该在对应的.mtl
文件中有对应的newmtl
:
src/main/resources/assets/fmltutor/models/block/metal_furnace.mtl:
newmtl cylinder
当然有的OBJ模型会顺带一个后缀名为.mtl
的文件,里面有的定义了诸如Map_Kd
等行用于绑定材质,这里我们不用管他,后面我们再说绑定材质的方法。所以这里这个后缀名为.mtl
的文件只有若干个newmtl
就足够了。
现在打开游戏,如果能够看到放置在地上的方块形状和OBJ模型设定的一致(这里的是圆柱形),并且整个方块都是纯白色的话,那就说明目前的设置都是对的。
不过还有一点小小的问题,我们需要设置一下:
src/main/java/com/github/ustc_zzzz/fmltutor/block/BlockMetalFurnace.java(部分):
@Override
public boolean isFullCube()
{
return false;
}
@Override
public boolean isOpaqueCube()
{
return false;
}
以告诉原版Minecraft这个方块是透明的和不完整的,进而使游戏可以正确渲染其侧面的方块。
OBJ模型的材质绑定
我们先从绑定材质开始,为方便演示,这里先直接贴代码:
src/main/resources/assets/fmltutor/blockstates/gold_furnace.json:
{
"forge_marker": 1,
"defaults": {
"model": "fmltutor:metal_furnace.obj"
},
"variants": {
"inventory": [{
"textures": { "#cylinder": "fmltutor:blocks/gold_furnace_texture" }
}],
"burning": {
"true": {
"textures": { "#cylinder": "fmltutor:blocks/gold_furnace_burning_texture" }
},
"false": {
"textures": { "#cylinder": "fmltutor:blocks/gold_furnace_texture" }
}
}
}
}
src/main/resources/assets/fmltutor/blockstates/iron_furnace.json:
{
"forge_marker": 1,
"defaults": {
"model": "fmltutor:metal_furnace.obj"
},
"variants": {
"inventory": [{
"textures": { "#cylinder": "fmltutor:blocks/iron_furnace_texture" }
}],
"burning": {
"true": {
"textures": { "#cylinder": "fmltutor:blocks/iron_furnace_burning_texture" }
},
"false": {
"textures": { "#cylinder": "fmltutor:blocks/iron_furnace_texture" }
}
}
}
}
然后还有对应的四张贴图:
src/main/resources/assets/fmltutor/textures/blocks/gold_furnace_texture.png:
src/main/resources/assets/fmltutor/textures/blocks/gold_furnace_burning_texture.png:
src/main/resources/assets/fmltutor/textures/blocks/iron_furnace_texture.png:
src/main/resources/assets/fmltutor/textures/blocks/iron_furnace_burning_texture.png:
这里的贴图有一个要求,就是它们必须都是方形的。
细心的读者应该注意到了,这里的#cylinder
,就是之前我们在metal_furnace.mtl
里newmtl
中的cylinder
,注意下前面的井号。
OBJ模型的约定之一就是每一种newmtl
代表一种材质(或者颜色),只不过这种材质(或者颜色)可以在.mtl
后缀的文件中指定,在Forge Mod中,我们也可以在blockstates
文件夹下的,描述BlockStates文件里指定。
之前创建的所有gold_furnace
和iron_furnace
开头的,一共六个材质文件都可以删掉了,因为我们已经使用新的材质了。
OBJ模型的仿射变换
现在我们开始解决之前的一个还没有解决的问题:四个方向。因为现在游戏中的所有炉子都是一个方向的,然而如果我们打开F3会发现名为facing
的属性的设置却是正确的。这就是因为我们还没有进一步设置上面提到的,用于实时生成的getExtendedState
方法:
src/main/java/com/github/ustc_zzzz/fmltutor/block/BlockMetalFurnace.java(部分):
@Override
public IBlockState getExtendedState(IBlockState state, IBlockAccess world, BlockPos pos)
{
IExtendedBlockState oldState = (IExtendedBlockState) state;
TRSRTransformation transform = new TRSRTransformation(state.getValue(BlockMetalFurnace.FACING));
TileEntity te = world.getTileEntity(pos);
if (te instanceof TileEntityMetalFurnace)
{
// TODO
}
OBJState objState = new OBJState(Lists.newArrayList(OBJModel.Group.ALL), true, transform);
return oldState.withProperty(OBJModel.OBJProperty.instance, objState);
}
仿射变换的大概了解可以参见这里,其本质大概就是若干个矩阵的相乘,不过读者这里似乎不大有必要了解表示平移旋转等不同的矩阵是什么以及怎么相乘的,只需要使用Forge为我们提供的TRSRTransformation
类就可以了。
一个比较正常的设置OBJ模型旋转方向的代码,大概就是这个样子。OBJState
类的构造方法的前两个参数和OBJ模型的显隐有关,这里我们暂时不去管它,照着使用就可以了,最后一个参数就是我们刚刚提到的TRSRTransformation
,也就是这两句:
OBJState objState = new OBJState(Lists.newArrayList(OBJModel.Group.ALL), true, transform);
return oldState.withProperty(OBJModel.OBJProperty.instance, objState);
TRSRTransformation
类有一个十分常用的构造方法,它传入一个方向,最后生成一个用于表示围绕方块中心(即方块的xyz坐标各加0.5)的对应方向的旋转。这里的方向我们通过获取facing
属性对应的值获取,也就是这句:
TRSRTransformation transform = new TRSRTransformation(state.getValue(BlockMetalFurnace.FACING));
现在我们为了演示更加灵活的方块旋转功能,作为演示这里我们添加了一个特性:在炉子工作的时候旋转。我们首先在TileEntity的update
方法里设置旋转的角度,这里因为旋转角度是为OBJ模型的渲染服务的,所以大可不必在服务端执行对应的代码:
src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):
protected double rotationDegree = 0;
src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):
@Override
public void update()
{
if (!this.worldObj.isRemote)
{
ItemStack itemStack = upInventory.extractItem(0, 1, true);
IBlockState state = this.worldObj.getBlockState(pos);
if (itemStack != null && downInventory.insertItem(0, itemStack, true) == null)
{
this.worldObj.setBlockState(pos, state.withProperty(BlockMetalFurnace.BURNING, Boolean.TRUE));
int burnTotalTime = 200;
switch (state.getValue(BlockMetalFurnace.MATERIAL))
{
case IRON:
burnTotalTime = 150;
break;
case GOLD:
burnTotalTime = 100;
break;
}
if (++this.burnTime >= burnTotalTime)
{
this.burnTime = 0;
itemStack = upInventory.extractItem(0, 1, false);
downInventory.insertItem(0, itemStack, false);
this.markDirty();
}
}
else
{
this.worldObj.setBlockState(pos, state.withProperty(BlockMetalFurnace.BURNING, Boolean.FALSE));
}
}
else
{
IBlockState blockState = this.worldObj.getBlockState(this.pos);
boolean burning = blockState.getProperties().containsKey(BlockMetalFurnace.BURNING)
&& blockState.getValue(BlockMetalFurnace.BURNING).booleanValue();
if (burning || this.rotationDegree > 0)
{
this.rotationDegree += 11.25;
if (this.rotationDegree >= 360.0)
{
this.rotationDegree -= 360.0;
}
this.worldObj.markBlockRangeForRenderUpdate(this.pos, this.pos);
}
}
}
public float getRotation()
{
return (float) (this.rotationDegree * Math.PI / 180);
}
else
后面的就是我们新加的内容,每次更新都更新一次rotationDegree
的值:
else
{
IBlockState blockState = this.worldObj.getBlockState(this.pos);
boolean burning = blockState.getProperties().containsKey(BlockMetalFurnace.BURNING)
&& blockState.getValue(BlockMetalFurnace.BURNING).booleanValue();
if (burning || this.rotationDegree > 0)
{
this.rotationDegree += 11.25;
if (this.rotationDegree >= 360.0)
{
this.rotationDegree -= 360.0;
}
this.worldObj.markBlockRangeForRenderUpdate(this.pos, this.pos);
}
}
这里的有一句话比较关键:
this.worldObj.markBlockRangeForRenderUpdate(this.pos, this.pos);
这句话提醒游戏对该方块所在的位置重新渲染。
不过作者提醒一句,当这个炉子转动的时候,作者的游戏帧率瞬间下降了10%,所以说不要频繁使世界上的方块更新渲染,也就是限制上面这个方法的调用频率,好在教程只是为了演示,实际上的Mod中大部分的方块都是静止不动的,所以游戏不会那么卡。
然后我们声明一个方法去获取它:
public float getRotation()
{
return (float) (this.rotationDegree * Math.PI / 180);
}
并把它用在扩展BlockState的设置上:
src/main/java/com/github/ustc_zzzz/fmltutor/block/BlockMetalFurnace.java(部分):
@Override
public IBlockState getExtendedState(IBlockState state, IBlockAccess world, BlockPos pos)
{
IExtendedBlockState oldState = (IExtendedBlockState) state;
TRSRTransformation transform = new TRSRTransformation(state.getValue(BlockMetalFurnace.FACING));
TileEntity te = world.getTileEntity(pos);
if (te instanceof TileEntityMetalFurnace)
{
Matrix4f matrix = new Matrix4f();
matrix.rotY(((TileEntityMetalFurnace) te).getRotation());
transform = TRSRTransformation.blockCenterToCorner(new TRSRTransformation(matrix)).compose(transform);
}
OBJState objState = new OBJState(Lists.newArrayList(OBJModel.Group.ALL), true, transform);
return oldState.withProperty(OBJModel.OBJProperty.instance, objState);
}
首先我们声明了一个矩阵,并进行了旋转操作:
Matrix4f matrix = new Matrix4f();
matrix.rotY(((TileEntityMetalFurnace) te).getRotation());
然后我们在TRSRTransformation
的构造方法中传入了这个矩阵,以表示这个变换。不过这里的blockCenterToCorner
方法的调用就值得去思考了。
这是因为我们设置的旋转是围绕方块边缘,也就是方块的xyz坐标所在的位置的,然而这里我们想要的旋转是围绕方块中心的,一种常见的做法是:
- 把方块向xyz轴的负方向各移动0.5个单位,使其中心在方块的边缘
- 执行之前的围绕方块边缘的操作
- 把方块向xyz轴的正方向各移动0.5个单位,使其回到原先的位置
实际上,这个名为blockCenterToCorner
的方法,就这么做了,所以我们直接拿来用就可以了。
后面的这个compose
的方法,其实指的就是变换的复合,也就是把我们目前设置的旋转任意角度的变换和之前我们设置的旋转到对应方向的变换进行了复合,本质其实是两个矩阵的乘法:
transform = TRSRTransformation.blockCenterToCorner(new TRSRTransformation(matrix)).compose(transform);
最后我们把这个变换应用到扩展BlockState上去。
打开游戏试试吧~