1. Forge BlockState V1 格式
考虑这样一个方块状态定义,这个方块是一种加工设备,可以面朝东南西北,有开关两种工作状态:
{
"variants": {
"facing=north,status=on": { "model": ".../machine_on", "y": 0 },
"facing=north,status=off": { "model": ".../machine_off", "y": 0 },
"facing=south,status=on": { "model": ".../machine_on", "y": 180 },
"facing=south,status=off": { "model": ".../machine_off", "y": 180 },
"facing=west,status=on": { "model": ".../machine_on", "y": 270 },
"facing=west,status=off": { "model": ".../machine_off", "y": 270 },
"facing=east,status=on": { "model": ".../machine_on", "y": 90 },
"facing=east,status=off": { "model": ".../machine_off", "y": 90 }
}
}
这么一来总共有八种可能的 Block State。是不是感觉重复的内容特别多?简化的办法自然是有的——Forge 提供的 BlockState 格式。关于这个格式的说明并不多,甚至 ForgeDocs 上也只提及了一小部分。曾经负责 Forge 的渲染相关的 RainWarrior 曾写过一份它的规范,但留下的注释并不多。本文将基于这份规范撰写,并尽力给出清晰的解释。
1.1. 言归正传,来看简化后的版本
{
"forge_marker": 1,
"variants": {
"status": {
"on": { "model": ".../machine_on" },
"off": { "model": ".../machine_off" }
},
"facing": {
"north": { "y": 0 },
"south": { "y": 180 },
"west": { "y": 270 },
"east": { "y": 90 }
}
}
}
forge_marker 用来标记该 JSON 使用 Forge 的格式,并给出使用的格式的版本号。目前这个格式只有 V1 这一个版本,所以这里自然写了 1。然后就是不一样的地方了:facing 属性只定义了绕 Y 轴的旋转角度,而模型则只由 status 属性的值决定。Forge 会将这些属性反复组合起来生成所有可能的模型(其实就是个对这些属性求笛卡尔积的过程)。
Forge BlockState V1 格式保留了一定程度上的对原版格式的兼容;那个 y 的行为和原版的一致。自然,原版的绕 X 轴以 90 度为单位旋转在 Forge BlockState V1 中也可以使用。甚至最开始那个原版格式的 JSON 加上 "forge_marker": 1 也符合 Forge BlockState V1 格式。
1.2. 默认属性
Forge BlockState V1 格式除了语法更简洁外,还支持更多特性。还是考虑刚才那个加工设备。如果它没有开关的状态,那么不论面朝哪里它都会是同一个模型,只是需要根据朝向转一转。Forge BlockState V1 允许你指定“默认值”:
{
"forge_marker": 1,
"defaults": {
"model": ".../machine"
},
"variants": {
"facing": {
"north": { "y": 0 },
"south": { "y": 180 },
"west": { "y": 270 },
"east": { "y": 90 }
}
}
}
这里的默认值便是要使用的模型。于是 Forge 会拿四种可能的状态与默认值相组合,得到最终对应的模型。直接在这里指定纹理路径也是可能的。
1.3. 子模型
Forge BlockState V1 格式允许你使用若干子模型,换言之可以把好几个互相独立的方块模型用这种方式拼起来。
{
"forge_marker": 1,
"variants": {
"status": {
"on": {
"model": ".../machine_on",
"submodel": {
"part1": {
"model": ".../extended_part"
}
}
},
"off": { "model": ".../machine_off" }
},
"facing": {
"north": { "y": 0 },
"south": { "y": 180 },
"west": { "y": 270 },
"east": { "y": 90 }
}
}
}
这样一来,这个设备工作的时候就会“长”出点东西。part1 在这里只是起到一个命名的作用;如果你需要多个子模型,你大可以在 submodel 里塞 part2、part3、……
唯一的遗憾是子模型似乎不能在 defaults 中使用,原因未知。
1.4. 仿射变换(Affine Transformation)与 TRSRTransformation
Forge BlockState V1 格式还允许你对模型(自然也包括子模型)做仿射变换。为简明起见,从这里开始,本文不给出完整的 BlockState JSON。
{
"...": "...",
"defaults": {
"model": ".../target_model",
"transform": [
[ 1, 0, 0, 0 ],
[ 0, 1, 0, 0 ],
[ 0, 0, 1, 0 ]
]
},
"...": "..."
}
这个增广矩阵最右一列用于平移,左侧的 矩阵是只包括旋转和缩放的变换矩阵。也可以写成这样:
"transform": {
"matrix": [
[ 1, 0, 0, 0 ],
[ 0, 1, 0, 0 ],
[ 0, 0, 1, 0 ]
]
}
但是这样写起来的可读性多数时候并不好。不过,还有另一种形式的写法可用:TRSRTransformation。TRSR 是 "translation, rotation, scale, rotation" 的首字母缩略词。
{
"...": "...",
"defaults": {
"model": ".../target_model",
"transform": {
"translation": [ 0, 0, 0 ],
"rotation": [ { "x": 0 }, { "y": 0 }, { "z": 0 } ],
"scale": [ 1, 1, 1 ],
"post-rotation": [ { "x": 0 }, { "y": 0 }, { "z": 0 } ]
}
},
"...": "..."
}
其中:
- 所有字段都是可选的,不需要的变换都可以直接省略。
rotation和post-rotation字段可使用四元数[ x, y, z, w ]。rotation和post-rotation字段亦可直接赋值为类似{ "x": 0 }的形式,表示只在唯一一个指定的轴上转。scale可直接赋值为一个数,表示 uniform scale。使用[ 1, 1, 1 ]形式则表示在对应的轴上缩放(x、y、z)。
1.5. 不同视角下的变换
transform 字段可以定义不同视角下需要应用的不同变换;这个特性类似于原版方块模型中的 display 字段,但相比原版方块模型,Forge BlockState V1 的版本可以使用仿射变换或 TRSRTransformation。
{
"...": "...",
"transform": {
"thirdperson": { "matrix": "..." },
"thirdperson_lefthand": { "matrix": "..." },
"thirdperson_righthand": { "matrix": "..." },
"firstperson": { "matrix": "..." },
"firstperson_lefthand": { "matrix": "..." },
"firstperson_righthand": { "matrix": "..." },
"head": { "matrix": "..." },
"gui": { "matrix": "..." },
"ground": { "matrix": "..." },
"fixed": { "matrix": "..." }
},
"...": "..."
}
其中 thirdperson 是 thirdperson_righthand 的 shortcut,firstperson 是 firstperson_righthand 的 shortcut,剩余字段的含义可直接对照 Minecraft Wiki 上的说明来理解。
1.6. 预设的变换模版
上文中提到的 transform 字段其实还可以直接赋值为 String,此时 Forge 会使用对应的预设模板对模型进行变换。
{
"...": "...",
"defaults": {
"model": ".../target_model",
"transform": "identity"
},
"...": "..."
}
identity 自然是返回模型它本身的单位变换。其他可用的变换还有 forge:default-block(用于方块及 ItemBlock)、forge:default-item(用于一般物品)和 forge:default-tool(用于镐、斧这样的工具)。
有一点需要注意:forge:default-block、forge:default-item 和 forge:default-tool 已经完整定义了 thirdperson、firstperson、head、 ground、gui、fixed 等等场景下的变换,所以上文中提到的“不同视角下的变换”是不能用这三个模板的,只有 identity 可用。想想看也能知道允许的话会显得十分奇怪。
1.6.1. ForgeBlockStateV1.Transforms
14.23.5.2772 中你可以通过 ForgeBlockStateV1.Transforms 的 get 方法拿到上文中提到的 forge:default-block、forge:default-item 和 forge:default-tool 三个模板对应的 IModelState 对象。如此一来,可以通过 IModelState 的 apply 方法拿到不同视角下的 TRSRTransformation(封装在一个 Optional 中,因为的确有可能不存在),在某些时候这个数据相当有用。
1.7. 物品模型使用 Forge BlockState V1
没错,四种可用的变换模板里的 forge:default-item 实际上是可以给物品用的。具体来说,是这样操作的:
ModelLoader.setCustomModelResourceLocation(myItem, 0, new ModelResourceLocation(new ResourceLocation("my_mod", "example_item_model"), "inventory"));
实际上这个 ModelResourceLocation 也可以代表一个 BlockState JSON 的位置。比如上面这个例子中的 ModelResourceLocation 也可以指向 assets/my_mod/blockstates/example_item_model.json 中定义的 variants.inventory。事实上,Minecraft/Forge 也的确是会搜索这里的。
但这里有一个坑:variants 同时也包含了所有方块状态中的属性列表,直接声明成这样会有歧义:
{
"...": "...",
"variants": {
"inventory": { "...": "...", "transform": "forge:default-item" }
}
}
这究竟是一个完整的 variants,还是一个方块状态中的特定属性?Forge BlockState V1 没有解决这个问题,但这个问题有一个 workaround:
{
"...": "...",
"variants": {
"inventory": [{
"...": "...",
"transform": "forge:default-item"
}]
}
}
注意到数组的声明。在原版的 BlockState JSON 格式中这代表从数组中随机抽一个出来作为实际使用的模型。因为某些原因,使用这样的声明要求对应的键必须是完整的 variant 名,因此这样写可以消去歧义。