用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:液體磚塊的渲染至今仍是硬編碼,實體的渲染也全是硬編碼.