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 里塞 part2part3、……
唯一的遗憾是子模型似乎不能在 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 ]
        ]
    },
    "...": "..."
}

这个增广矩阵最右一列用于平移,左侧的 3×33 \times 3 矩阵是只包括旋转和缩放的变换矩阵。也可以写成这样:

"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 } ]
        }
    },
    "...": "..."
}

其中:

  • 所有字段都是可选的,不需要的变换都可以直接省略。
  • rotationpost-rotation 字段可使用四元数 [ x, y, z, w ]
  • rotationpost-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": "..." }
    },
    "...": "..."
}

其中 thirdpersonthirdperson_righthand 的 shortcut,firstpersonfirstperson_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-blockforge:default-itemforge:default-tool 已经完整定义了 thirdpersonfirstpersonheadgroundguifixed 等等场景下的变换,所以上文中提到的“不同视角下的变换”是不能用这三个模板的,只有 identity 可用。想想看也能知道允许的话会显得十分奇怪。

1.6.1. ForgeBlockStateV1.Transforms

14.23.5.2772 中你可以通过 ForgeBlockStateV1.Transformsget 方法拿到上文中提到的 forge:default-blockforge:default-itemforge:default-tool 三个模板对应的 IModelState 对象。如此一来,可以通过 IModelStateapply 方法拿到不同视角下的 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 名,因此这样写可以消去歧义。