70
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

Organization

クラフトピアVRMmod開発の経緯と作り方をまとめてみた

はじめに

クラフトピアVRMmod作者の@yoship1639です。時間の合間を縫って久々の投稿です。

以前クラフトピアのプレイヤーモデルをVRMアバターに差し替えるmodを作ったら思った以上に反響がありました。
なので、VRMmodを作成した経緯とどうやって作成したかをまとめてみたいと思います。
がっつりした内容ではないです。

クラフトピアとは

クラフトピアは、簡単にまとめると「クラフト農業ハクスラ自動化建築マルチオープンワールドサバイバルアクションゲーム」です。
面白そうな要素をとにかく詰めこんだクラフトゲーです。※2020/10/28現在はアーリーアクセス版です。

クラフトピア
公式サイト:https://www.pocketpair.jp/?lang=ja

VRMmodとは

私が開発したVRMmodは、クラフトピアのプレイヤーモデルを任意のVRM形式のアバターに差し替えるmodです。
これにより、VRMアバターさえ準備すれば、自分の好きなモデルでゲームをプレイする事が出来ます。

2020/10/28現在ver1.3.6で、githubにオープンソースとして公開しています。
https://github.com/yoship1639/Player2VRM
VRMmod

VRMmod作成の経緯

大して深い話じゃないですがVRMmodの経緯を簡単に説明します。

最初はmodが作れることすら知らなかった

当初mod作成の経験はほとんどなく(Minecraftのmod作りをちょこっと触った程度)、そもそもクラフトピアでmodが作れるとも知りませんでした。なので、最初からmodを作ろうとも思っていませんでした。

クラフトピアを1ユーザーとして楽しんでいたのですが、まだアーリーアクセスという事で出来ることに限りがあり、プレイすればするほどやることが無くなってきました。そこで、何かまだ面白い事が出来ないか模索していたら公式のDiscordがあることを知りました。

Discordでは雑談・質問・要望などあらゆるチャンネルが存在し、その中にmod開発のチャンネルがあるのを見つけました。ここで初めてクラフトピアでmod開発が出来ることを知りました。

やることもなくなってきて、以前Minecraftで簡単なmod作りを行ったことがあったので、どうせならmodで面白いこと出来ないかと思い立ち興味本位で触ってみたのがmodを作るきっかけとなります。

VRMmod開発は昔作ってたゲームがきっかけ

modの作り方や考え方はある程度知っていたので、開発環境の構築に手間取ることはなくすぐに開発できる段階まで準備が整いました。

しかし、ある重大なことに気が付いてしまいました。「ヤバい…作りたいものが特にない…」

折角環境用意したのに作りたいものが最初は思い浮かびませんでした。ハクスラゲーであれば能力値引き上げやアイテム個数を増やしたりする所謂チート系が思い浮かびますが、チート系はゲームをすぐに飽きさせるを知っていたので手を出そうとは全く思いませんでした。次に、インベントリ拡張など便利系のmod作りが思い浮かびましたがこれも既に作られていて、便利系も飽きさせ易いことを知っていたのでこれもスルーしました。最後に思いついたのはキャラクタや敵の見た目を変える見た目系のmodで、これなら特に飽きさせるとかなくただ面白いだけで済むのでこれ系で何か作れないか考えました。

ただ、見た目を変えるmodを作ろうと思ったら、通常差し替えるモデルを準備しないといけません。私は特にモデラーではないので用意するモデルは無く、どうしようか考えていたら、昔作ったゲームを思い出しました。

昔作ってたのはMikuMikuWorldというMMDモデルを動的に読み込んで好きなMMDモデルでゲームをマルチプレイで楽しめるというものです。C#+OpenGL4でPBRレンダリングできるハイクオリティなゲームとして頑張って作っていましたが、1人で作るにはあまりに大変すぎるという事で断念しました。

そこで、モデルをこちらで準備するのではなく使う側で準備してもらえば、好きなモデルで遊べるし私も楽できるのでwinwinだと思い立ったのがVRMmodを作るきっかけとなります。

出来るかは分からなかった

幸い、VRMを読み込む機能自体はUniVRMとしてライブラリとして提供されているので、これを実装する手間は省けました。
https://github.com/vrm-c/UniVRM
UniVRMはコントリビューターとしてちょこっとだけ参加させていただいていたので使い勝手は大体解っていました。

最初は、modとしてではなくクラフトピアのManagedDLLを直接いじる所から始めました。dnSpyを使ってManagedDLLの処理を書き換え、UniVRMを取り込みVRM読み込みが出来るか確認し、これは問題なく出来る事が解りました。

ManagedDLLをいじってVRMが読み込める事が解ったので、次は汎用的に扱えるmod化を試みました。しかし以下の問題がありました。

  • BepInExプラグイン(mod)上でUniVRMのDLLを問題なく読み込めるか
  • クラフトピア上のavatarやanimatorと同期できるか

上記が出来なければVRMmodは作れませんでしたが、実装を進めるうちに、問題なくUniVRMをプラグインとして読み込める、そしてクラフトピア上のavatar, animatorと同期が出来る事が解り「あ、これ行けるな」と確信に変わりました。

こうして、良い感じに実装を進めパッケージ化してVRMmodの初版が出来上がりました。
後は、公開したら想像に反して何か凄いことになっちゃった感じです。

VRMmodの作り方

ここからは技術的な話になります。

Unity製ゲームのmod作りに必要なもの

ゲームの中には最初からmod用の窓口が用意されているものもありますが、そうでない場合Unity製のアプリのmodを作るには以下を準備する必要があります。また、Unity製のゲームといってもビルドがIL(中間言語)の状態でなければならないので注意してください。

  • BepInEx (ILの処理を追加・変更できるプラグイン)
  • dnSpy (ILの中身を見れるツール)
  • Visual Studio

VRMmodはILの処理を書き換えるプラグインとしてBepInExを使いました。BepInExはUnity製のゲームにパッチやプラグインを導入できるようにするフレームワークです。導入もパッチの当て方も比較的簡単です。(というより他のやつを知らない…)
dnSpy

ILの中身を確認できなければ何もできないのでツールを使ってプログラム処理を解析します。ILの中身は今回はdnSpyで確認しました。dnSpyは.Netのアセンブリを見る事も編集することもできます。mod化する前はこれで直接アセンブリを編集して動確していました。
dnSpy

後は実際にmodを作成するためのエディタ環境です。これは普通にVisual StudioでOKです。

modを作ってみる

mod作りに必要なものが準備出来たら、実際にmodを作っていきます。

まず、Visual Studioで.NetFrameworkのクラスライブラリ(DLL)プロジェクトを作ります。このdllがmod本体になります。
プロジェクトを作成したら以下を参照に加えます。

  • BepInEx.dll
  • 0Harmony.dll
  • UnityEngine.dll
  • UnityEngine.CoreModule.dll
  • Assembly-CSharp.dll

BepInEx.dllと0Harmony.dllは導入したBepInExから参照、UnityEngine.dll, UnityEngine.CoreModule.dll, Assembly-CSharp.dllはパッチを当てたいゲームのところから参照させます。クラフトピアの場合は以下のようになります。

  • Craftopia/BepInEx/core/BepInEx.dll
  • Craftopia/BepInEx/core/0Harmony.dll
  • Craftopia/Craftopia_Data/Managed/UnityEngine.dll
  • Craftopia/Craftopia_Data/Managed/UnityEngine.CoreModule.dll
  • Craftopia/Craftopia_Data/Managed/Assembly-CSharp.dll

参照はそのmodに必要なものを随時追加してください。
参照を追加したら、modのメインプラグインであるMainPlugin.csを作成します。

MainPlugin.cs
using BepInEx;
using HarmonyLib;

namespace Player2VRM
{
    [BepInPlugin(PluginGuid, PluginName, PluginVersion)]
    [BepInProcess("Craftopia.exe")]
    public class MainPlugin : BaseUnityPlugin
    {
        public const string PluginGuid = "com.yoship1639.plugins.player2vrm";
        public const string PluginName = "Player2VRM";
        public const string PluginVersion = "1.0.0.0";

        void Awake()
        {
            // Harmonyパッチ作成
            var harmony = new Harmony("com.yoship1639.plugins.player2vrm.patch");
            // Harmonyパッチ全てを適用する
            harmony.PatchAll();
        }
    }
}

BaseUnityPluginをベースにしたクラスのAwakeで以降に作成するパッチをすべて適用する記述をしたら下準備完了です。
次に、処理を書き換えるパッチクラスを作ります。

Player2VRM.cs
using HarmonyLib;
using Oc;

namespace Player2VRM
{
    [HarmonyPatch(typeof(OcPl))]
    [HarmonyPatch("charaChangeSteup")] // Steupは運営のtypoのまま
    static class Patch_OcPl_charaChangeSteup
    {
        // 元のcharaChangeSteupが呼ばれる前に呼ばれるメソッド、戻り値がtrueだと元のメソッドが呼ばれ、falseだと呼ばれない
        static bool Prefix(OcPl __instance)
        {
            UnityEngine.Debug.Log("charaChangeSteup Prefix");
            return true;
        }

        // 元のcharaChangeSteupが呼ばれた後に呼ばれるメソッド
        static void Postfix(OcPl __instance)
        {
            UnityEngine.Debug.Log("charaChangeSteup Postfix");
        }
    }
}

処理を編集・追加したいクラスメソッドを指定し、メソッドの前に呼びたい場合はPrefix, メソッドの後に呼びたい場合はPostfixを指定します。この場合、処理が呼ばれる時に追加でデバッグログが出力される感じになります。

modの基本形ができたので、実際にVRMを読み込む処理を実装してみます。

簡単なVRMmodを作ってみる

Unity製ゲームでVRMを読み込むためのライブラリであるUniVRMに含まれる以下のDLLを準備します。適当なUnityプロジェクトを作成し、UniVRMのリリースページにあるunitypackageをインポートすると以下のDLLがアセットとして導入されます。

  • DepthFirstScheduler.dll
  • MeshUtility.dll
  • MToon.dll
  • UniHumanoid.dll
  • UniJSON.dll
  • UniUnlit.dll
  • VRM.dll

これらを、modプロジェクトの参照として追加します。これでUniVRMの機能がmodプロジェクトで読み込めるようになります。(ちなみに2020/10/28現在ではUniVRMはまだ正式版がリリースされていないため仕様が大きく変わる可能性があります)

次に、VRMを読み込むコードを以下の様に記述します。(動確していないので動かないかもしれません)

Player2VRM.cs
using HarmonyLib;
using Oc;
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using VRM;

namespace Player2VRM
{
    [HarmonyPatch(typeof(OcPl))]
    [HarmonyPatch("charaChangeSteup")]
    static class Patch_OcPl_charaChangeSteup // Steupは運営のtypoのまま
    {
        private static readonly string VrmPath = Environment.CurrentDirectory + @"\Player2VRM\player.vrm";
        private static GameObject vrmModel;
        private static List<OcPl> alreadyVrmSetList = new List<OcPl>();

        // 元のcharaChangeSteupが呼ばれた後に呼ばれるメソッド
        static void Postfix(OcPl __instance)
        {
            UnityEngine.Debug.Log("charaChangeSetup Postfix");

            // VRMモデルが読み込まれていなかったら
            if (vrmModel == null)
            {
                // VRMモデル読み込み
                vrmModel = ImportVRM(VrmPath);
            }

            // VRMモデルをまだセットしていないPlayerインスタンスの場合
            if (!alreadyVrmSetList.Contains(__instance))
            {
                // VRMモデルを複製
                var model = GameObject.Instantiate(vrmModel);

                // 親を既存モデルにセット
                model.transform.SetParent(__instance.transform, false);

                alreadyVrmSetList.Add(__instance);
            }

            // 既存モデルのVRMモデル以外のSkinnedMeshRendererを非表示
            foreach (var smr in __instance.GetComponentsInChildren<SkinnedMeshRenderer>())
            {
                smr.enabled = false;
                Transform trans = smr.transform;
                while (vrmModel != null && trans != null)
                {
                    if (trans.name.Contains(vrmModel.name))
                    {
                        smr.enabled = true;
                        break;
                    }
                    trans = trans.parent;
                }
            }
        }

        private static GameObject ImportVRM(string path)
        {
            var bytes = File.ReadAllBytes(path);
            var context = new VRMImporterContext();
            context.ParseGlb(bytes);

            try
            {
                context.Load();
            }
            catch { }

            return context.Root;
        }
    }

    [HarmonyPatch(typeof(OcPlHeadPrefabSetting))]
    [HarmonyPatch("Start")]
    static class Patch_OcPlHeadPrefabSetting_Start
    {
        static void Postfix(OcPlHeadPrefabSetting __instance)
        {
            // 既存モデルの頭部を非表示にする
            foreach (var mr in __instance.GetComponentsInChildren<MeshRenderer>())
            {
                mr.enabled = false;
            }
        }
    }
}

アニメーションの同期等はこれだとまだ未実装ですが、実はこれだけでクラフトピアのプレイヤーモデルはVRMモデルに差し替わります。思ったより簡単だと思いませんか?それだけUniVRMは簡単にVRMが読み込めるように作られているのです。

後は細かい調整と機能の追加をしてビルドすれば簡易VRMmodの出来上がりです。

オープンソースで作ったらどうなったか

前にも述べましたがクラフトピアVRMmodはオープンソースで公開しています。オープンソースにした理由は単純で、私一人の力だと限界があると思ったからです。

実際にオープンソースにしたら改良のプルリクをたくさんいただきました。MToonシェーダ等が正しく読み込まれなかった問題の修正や、モデルのアニメーションが不安定だった問題の修正、簡易マルチプレイに対応できるようにする機能追加など、私一人だと解決できなかった様々な問題を優秀なエンジニアの方に直していただきました。そのおかげもあり、VRMmodは初期と比べて非常に安定した動作をするようになりました。

何が言いたいのかというと、自分ひとりの力を過信するのはあまり良くないという事を言いたいです。どんなに自分の能力に自信があっても、解決できない事はたくさんありますし視野も狭くなりがちです。世の中には優秀なエンジニアの方がたくさんおりますので、それらの方々の目に留まる形で公開するとほぼ必ず良い方向に向かうので(実際私は多くの知見を得る事が出来ました)、他の人に頼る・見てもらうこともエンジニアとしての成長に大きく繋がるのではないかと思います。

おわりに

あまり経緯を綴る形の記事を書いたことはなかったので少し躊躇しましたが、この記事で色んな方にVRMは非常に簡単にUnity上で実装できることを知ってもらえたらと思います。

私はVRMをmodで読み込める仕組みをたまたま最初期に見つけただけで、特にこの知見を独占するつもりは更々ないので、これを機にVRM関連の開発が活発になってもらえたらと思います。

VRMmodを作ったことによるメリット

おわりに の後に持ってくる話ではないですが、VRMmodを作るとどんな良い事があるかを書けばVRM開発を始めようとする人も出てくるのではと思ったので書いておきます。

  • VRMの使いどころがない迷える子羊を救う事が出来る
  • ついったーのフォロワーが2000人以上増える
  • Vtuberの方々が自分の作ったVRMmodで遊んでくれる
  • ニュース記事を書いてもらえる
  • 企業からオファーが来る
  • 自分の実績になる
  • にじさんじのあまみゃ、ホロライブのロボ子さんにフォローしてもらえる

はい、これだけのメリットがあります。頑張りたくなりますよね!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
70
Help us understand the problem. What are the problem?