用MinecraftForge导入外部模型

之前有人问到如何导入.obj和.b3d模型,本来我是想写在Extra编的MC3D图形部分,不过因为最近没什么时间完成全篇,因此就先单独把这部分拿出来写完.

在1.8之前,Forge支持载入.obj和.tcn(Techne)模型.而到了1.8的时候,Minecraft渲染系统的变化破坏了之前的模型机制,在Lex苦于反混淆工作,无力处理模型系统的时候,一个叫RainWarrior的大光头从人群中站了出来,给Lex安利了一套自己写的ModelSystem,新的模型系统目前只自带.b3d格式的支持(要不要给它写个.obj或MMD模型支持?[注1]笑...),这个格式我还是头一次听说,不过看上去功能倒一点也不少,居然还支持骨骼动画卧槽...堪比.fbx了啊.

先说一下1.7的模型系统,核心的东西就net.minecraftforge.client.model包下的4个类(其中还有一个是异常类...),AdvancedModelLoader类是模型系统的封装,负责注册模型加载器以及接收加载模型的请求并将它派发给相应的加载器(或抛出异常,如果没有合适的加载器的话);IModelCustomLoader接口是模型加载器,负责实际处理加载请求[注2];IModelCustom接口代表加载到的模型,Forge不关心模型在内部是怎么实现的,只要它能正确渲染就行.

先看一下AdvancedModelLoader类有什么内容,它包含了3个公共静态方法:registerModelHandler方法用来注册模型加载器,普通使用者可以无视它;getSupportedSuffixes方法查询支持载入什么后缀名的模型;关键在于loadModel方法,它用来载入模型,它的参数是一个ResourceLocation,代表模型的位置,直接使用2个参数的构造函数创建即可(第一个是资源文件夹的名字,第二个是相对路径,比如你的模型放在assets/mymod/models/mymodel.obj,那么第一个字符串是"mymod",第二个是"models/mymodel.obj").

然后就剩下IModelCustom需要搞定了,它包含了4个方法:renderAll,renderOnly,renderPart和renderAllExcept,这都是什么鬼啊.
俗话说得好,想要弄明白一个游戏引擎的模型/渲染系统的架构是怎么/为什么设计的,那就去研究它的首选模型格式,WavefrontObject和Techne都支持将一个模型分组成子模型,因此也不奇怪为什么Forge的模型系统被设计为支持分组渲染.

至于渲染,砖块使用ISimpleBlockRenderingHandler接口写一个渲染类然后用RenderingRegistry的registerBlockHandler方法来注册(RenderId通过重写砖块类的getRenderType方法来指定);物品使用IItemRenderer接口写一个渲染类然后用MinecraftForgeClient的registerItemRenderer方法来注册;实体则是写一个继承Render类的渲染类然后用RenderingRegistry的registerEntityRenderingHandler方法来注册.

然后是1.8的模型系统,相比旧系统来说,新系统实在是...让人"叹为观止",仅仅是核心部分就有15个类或接口.一个强健的系统固然是好的,作者设计之初想必也有自己的理由,但如此复杂的架构未免还是有些太复杂了吧[注3]...
坐而言不如起而行,先来一个个剖析一下那15个核心类.
首先是ModelLoaderRegistry,它的作用相当于原来的AdvancedModelLoader,用来加载模型和注册模型加载器.
然后是IModel和IModelPart,不过前者并不相当于原来的IModelCustom,它代表一个刚刚解析完,尚未将数据传入显存的模型;而最奇怪的设计是前者继承自后者,即IModel extends IModelPart,按作者的说法,IModelPart代表一个最小的单元,而它本身是个纯粹的标签接口,里面没有任何内容,仅仅是指代表"实现了这个接口的都是模型的一部分"...
那么什么是一个数据已经被传入显存,随时可以渲染的模型?MC1.8自带了一个IBakedModel类,位于net.minecraft.client.resources.model包下,它代表一个可以被用于渲染的模型.但Forge不推荐我们直接使用它,取而代之的,Forge从IBakedModel那又扩展出4个接口:IFlexibleBakedModel,一个补充了泛型的封装;IPerspectiveAwareModel,针对随镜头透视而变化的模型的封装;以及ISmartBlockModel和ISmartItemModel,针对那些根据砖块状态和物品状态而改变的模型的封装.
还有个奇怪的接口是IModelState,按官方的解释是代表模型当前的状态...跟它相关的有个是ITransformation接口,用来表示模型的位置变换.
下面还有几个普通使用者不用管的类/接口:
Attributes类代表模型的顶点数据格式.
IColoredBakedQuad接口代表该面自带颜色.
ICustomModelLoader接口就相当于老版的IModelCustomLoader.
ModelLoader类用于将新的模型系统适配到MC1.8自带的模型系统.

然后的问题就是如何显示它们了,对砖块和物品来说并不难,MC1.8采用了数据驱动的渲染系统,也就是说大部分[注4]跟渲染相关的代码都被去掉,改成了用外部数据文件来表示.MC1.8的砖块渲染是写在blockstates中的json中(假设你现在已经了解1.8的砖块渲染了),一个最简单的砖块渲染文件是:

{
    "variants": {
        "normal": { "model": "[modid]:[无后缀的模型文件名]" }
    }
}

那个模型文件是个位于"assets/[modid]/models/block"中的json文件,如果我们想把一个B3D模型作为砖块模型的话,就把那个B3D文件置于"assets/[modid]/models/block"中,然后在砖块的渲染文件改成:

{
    "variants": {
        "normal": { "model": "[modid]:[有后缀的B3D文件名]" }
    }
}

比如官方测试用例中的:

{
    "variants": {
        "normal" : { "model" : "forgedebugmodelloaderregistry:untitled2.b3d" }
    }
}

对于模型使用的纹理,则放入"assets/[modid]/textures"中.
不过还有两步没有完成,首先你需要在B3D模型加载器中添加你的Mod素材目录,办发是在Mod主类的PreInit中添加:

B3DLoader.instance.addDomain("[modid,比如上文的'forgedebugmodelloaderregistry']");

到此砖块模型已经可以渲染了,但是我们都知道每一个砖块其实都有一个对应的物品,此时你的砖块在物品栏中是会被显示为一个紫黑格子,要添加物品渲染,需要加入:

Item item = Item.getItemFromBlock([砖块实例]);
ModelBakery.addVariantName(item, "[modid]:[模型文件,比如untitled2.b3d]");
ModelLoader.setCustomModelResourceLocation(item, 0, new ModelResourceLocation("[modid]:[模型文件]", "inventory"));

然后一个使用B3D模型的砖块便制作完了,更详细的内容可以参照官方的测试用例,里面还包括了如何使用metadata...不对,blockstate来改变模型.

制作一个使用模型的物品和上文的为砖块的物品设置模型的方式相同,只不过将item换成物品的实例即可.

至于实体的渲染...看上去现在的模型系统完全没考虑实体的渲染...如果真有人关心如何在实体渲染中使用模型的话我再去研究一下 (╱ロ゜)╱ (当然如果你知道怎么弄的话也可以留言告诉我让我补上:D)

注释:
[1]支持MMD模型的Forge:说起来还有个黑历史,当初我还脑子发热想给Forge写个.pmd模型格式的扩展,不过被ici2cc及时拦下了 233
[2]其实如果用设计模式解释的话,这就是个抽象工厂模式...
[3]那句话怎么说来的?好的设计不是再也没有功能可添,而是再也没有功能可减.
[4]Ugly hack:液体砖块的渲染至今仍是硬编码,实体的渲染也全是硬编码.