3Dモデルの調整をしながらVRChat用モデルからVRMへ効率良く変換する

Blenderで制作して完成したと思っても、実際に動かしてみたりするとウェイトペイントが上手くいってなくて服が突き抜けたり、テキスチャがおかしかったりと、様々な問題が発生してBlenderでの修正を再三に迫られる場面は多いです。
そのためにBlender側で修正したものを出力してUnityに読み込ませます。そのUnityへ読み込ませる際にちょっとした工夫をすると作業量を緩和できます。

この記事は多分OUCCアドベントカレンダーの最終日の記事になります。

プロローグは暇な人だけ読んでね

それ以外の人は本題

~プロローグ?~

~3Dモデル制作以前

去年のハッカソンでは、何かと不満を言われるE-Learningと和解しようという企画にて、自然言語で会話できるBotを制作しました。
その過程で会話相手として、E-Learningの化身、angEL-reinちゃん(名前はE-Learningのアナグラムから)が生まれました。私が生みました(生産者表示)
この名前微塵も流行らず、代わりにいーちゃんと呼ばれているのですが、このいーちゃんを音声で入出力して会話できるようにして学祭に出そうという話になりました。
そこで私はRustサーバーを作って非常に遅い言語生成処理の高速化を行ったりしたわけですが、そんな話は今回どうでも良く、それよりやはり音声での会話をするならば、会話する相手が見えた方がいいですよね?古来より偶像崇拝があるように概念を相手にするよりかは、視覚的に見えるものを相手にした方が良いに違いない。
というわけで、れいんちゃんの3D化計画を極秘裏に進めました。
以前の記事で既にLive2D化した話はしましたが、我慢できずに3D化に踏み切ってしまいました。

3Dモデル完成(仮)まで

まずVROIDで素体を生成し、Blenderで服などを制作しました。
私は3Dモデルを作るのはこれで2回目なので初めてというわけでは無いですが、やはりそれなりに苦戦はしました。特に前回と同様ウェイトペイントでは常に苦戦させられ、今回も最後まで難敵となりました。
まあ兎に角色々あってFBXモデルが完成しました。
VRChatに出したかったのでまずVRChat用の改変をしつつシェーダの調節をしていきました。
その結果完成したのがこちら
れいんちゃん
れいんちゃん
れいんちゃん
ちなみに私は目にこだわる人なので、目は虹彩を奥に配置し、反射光を前面に出すなど立体的にしています

その後単眼カメラによる姿勢推定アプリを制作する、又はキャラクタを後世の人に使ってもらうためにVRMモデルへの変換を行いました。

本題

 Blenderで制作して完成したと思っても、実際に動かしてみたりするとウェイトペイントが上手くいってなくて服が突き抜けたり、頂点にウェイトが割り当てられてなかったり、テキスチャがおかしかったりと、様々な問題が発生してBlenderでの修正を再三に迫られる場面は多いです。
 そのためにBlender側ではLazy Weight Toolなどを使って個々の頂点についてウェイトを塗りなおしたりして修正したものを出力してUnityに読み込ませます。そのUnityへ読み込ませる際にちょっとした工夫をすると作業量を緩和できます。

fbxファイルのBlender→Unity

 fbxファイルをunityに読み込むと、モデルをScene上に配置できるようになります。
VRChatを想定した場合、その配置したモデルにPhysboneなどのコンポーネントを設定していくことになります。
 しかしこの方式では、後で修正をかけたモデルで上書きして読み込む際にfbxファイルと共にScene上のオブジェクトもリセットされてしまい、またコンポーネントの設置などをしなければならなくなります。
 これでは非常に非効率なので、fbxファイルを上書きで読み込んだ際にScene上のオブジェクトのコンポーネントがはがれないようにしたいです。

 そんな時に便利なのがPrefab Variantです。
 Projectビューからfbxファイルを右クリック→Create→Prefab Variantで生成できます。
 この生成したものをドラッグアンドドロップでSceneに配置してコンポーネントを張り付けてみてください。
 こうすることでfbxファイルを上書き更新しても、Scene上のモデルだけが更新され、コンポーネントは剥がれなくなります。

VRChatモデル→VRMモデル

基本的には VRM Converter for VRChatを使用しますのでこれを既にUnityへインポートしている前提で記述します。

初めてのexport

 まず変換するVRChatモデルを選択した状態でツールバーのVRM0→Export VRM file from VRChat avatarで適当に入力してAssetディレクトリ内に用意したフォルダ(フォルダ1)にexportします。
 この際もしかしたら未使用シェイプキーにチェックを入れてexportした方がいいかもしれません。
 兎に角これでフォルダ1内にVRMファイルとそのprefabが生成されます。エラーが出ても、何も変えずにもう一度実行すると何故かうまくいったりします。
 Unity上だけで表情やマテリアル、SpringBoneなどの設定を変更するくらいなら、そのPrefabをSceneに配置・編集して、そのモデルを選択した状態でツールバーVRM0→export to VRM0でフォルダ1に出力すると、フォルダ1のVRMを上書き修正することができます。

2回目以降のexport

  1.  モデルを変更した場合、またVRChatモデルからVRMへの変換をする必要があります。
     この変換においてVRM0→Export VRM file from VRChat avatarで出力する場所はフォルダ1とは異なるフォルダ2へ出力した方が良いです。
     何故なら、フォルダ1に出力してしまうとせっかく以前に設定した表情や物理などのコンポーネントが上書きされてしまうからです。
     この時、フォルダ1には旧、フォルダ2に新モデルが入っていることになります。

  2. 生成された新モデルのPrefabファイルをSceneにドラッグアンドドロップで配置。
  3.  ツールバーのVRM0→Open CopyVRMSetting Wizardで以前のデータからSpringBoneや表情などの情報を新モデルへコピーします。
     この時に、Sorceにはフォルダ1の旧Prefabアセット(Projectビュー上)を選択し、Destinationには新モデルオブジェクト(Sceneビュー上)を選択しないとエラーが出ます。
     ここまででコピーできていないのはマテリアルのみです。
  4.  マテリアル情報を新モデルにコピーしていくのですが、使用マテリアルが多ければ多いほどこの作業が非常に面倒です。
     よってそれをやってくれるツールを自作しました。
     ここ(https://github.com/sachsen/TransferInspectorMaterials) から.csファイルを入手して、readmeに書かれている通りに導入してください。
  5.  ツールバーTools→Open Inspector Material Transfer Windowとします。
     sizeにコピーするSkinnedMeshRendererの数を入力し、Sorceにコピー元の、Destinationにコピー先のSkinnedMeshRendererを指定してCopy!ボタンを押すとマテリアル情報がコピーされます。なお、マテリアル自身は複製されません。
  6.  新しいモデルを選択してツールバーVRM0→export to VRM0でフォルダ1へ出力、上書きします。
     これでVRMファイルが新しくなって置き換わります。

完成

3Dモデルはこちらからぐりぐり見れます。

https://hub.vroid.com/characters/7942564502413041514/models/2799418893141239533

文化祭での様子

もっといい方法があるかもしれないですが、私はこの方法でやりました。

よければお使いください。

著者:上月

初学者のためのC#

C#とは

 C#はC++からJavaになる際に追加されたガーベージコレクション(GC)や中間言語を介して共通の実行環境で実行することなどを参考にMicrosoftが開発した言語です。C#はIL(中間言語)にコンパイル後.Net Framework上で実行されます。この.Net Framewokというのは標準でWindowsにインストールされており、そのためC#とその統合開発環境のVisual Studioを使うことで簡単にWindowsアプリケーションが開発・公開ができます。また、OUCCで主に使用しているUnityというゲーム作成に使われるフレームワークで使用する言語にも採用されています。

基本的な書き方

using System; // ファイル内でほかの名前空間のものを完全修飾型名を使わずに使用できるようにする

namespace Hoge // 名前空間はこうやって宣言する。
{
    public class Program // 処理は必ずクラスの中に関数を置いて書く
    { // 波カッコで範囲を示す

        public int HogeHoge { get { return _hogeHoge; } set { _hogeHoge = value; } } // プロパティの宣言方法
        private int _hogeHoge; // フィールドの宣言方法

        public static void Main()
        {
            Console.WriteLine("Hello World"); // 1文の最後は ; (セミコロン)で終わる
            var odds = Enumerable.Range(0, 100)
                .Where(i => i % 3 == 0)
                .ToArray(); // こんな感じで複数行にわたって書くことも可能。
            /*
            複数行コメントはこうやる
            */
        }
    }
}

StructとClass

 大きく分けてStructとClassの2種類があり、したのようにいろいろ違いはあります。StructはClassよりも動作が早いのですが参照型でないためにおこる様々な問題があるためオブジェクトを自分で定義する際は何か重要な問題がない限りClassで作ることをお勧めします。

structとclassの違い

struct class
オブジェクトの型 値型 参照型
データが保存されるメモリ スタック ヒープ
継承 interfaceの実装のみ class,interfaceともに可能
コンストラクタの定義 引数ありのものだけ可能 なんでも可能
nullableか nullを入れられない nullを入れられる

structの問題点

  • スタックに保存されるのでメモリを多く占有する大きなオブジェクトを入れると逆に遅くなる。
  • 参照型でないので関数やプロパティでとってきたものを変更してももとのオブジェクトを変更できない。(classでは可能)
  • 継承ができない
  • 引数なしのコンストラクタを作れない
  • nullを入れられない

他の問題を見たい方はC#に潜むstructの罠などを参考にしてみてください。

よく使う型

この中ではstringのみがclassでそのほかはstructです。

エイリアス 実装 リテラル 説明
byte System.Byte 0(収まる範囲内のみ) 符号なし整数(8bit) 主にバイナリデータのために使われます
short System.Int16 0(収まる範囲内のみ) 整数(16bit)
int System.Int32 0(収まる範囲内のみ) 整数(32bit)
long System.Int64 0(収まる範囲内のみ) 整数(64bit)
float System.Single 0f 小数型(32bit)
double System.Double 0.0又は0d 小数型(64bit)
decimal System.Decimal 0m 小数型(128bit)
bool System.Boolean true, false 真偽値(2bit)
char System.Char 'j' (シングルクォーテーションで囲む) UTF-16の文字(16bit)
string System.String "java" (ダブルクォーテーションで囲む) UTF-16の可変長文字列

使用例

int x = 1;
var x = 1; // varキーワードを使えば自動的に型を推測してくれる
System.Int32 x = 1; // こう書くこともできるが普通はしない

詳細(Microsoft公式ページ)

配列

C#で配列はclassであり、大きさは定義時に決定されます。書き方は以下の通りです。

int[] arr = new int[5]; //大きさ5の配列
string[] arr = new string[] { "a", "b", "c", "d", "e" }; //このように書いて初期化することも可能です。

var temp = arr[0]; //このように書くことで配列の要素にアクセスできます。

多次元配列

int[,] int[,,]のように書くことで多次元配列を作れます。たとえばint[3,4]とすると3×4多次元配列になります。

ジャグ配列

int[][]のように書くことでジャグ配列(ギザギザ配列)を作れます。これは配列の配列となっているので内側の配列の要素の数はそれぞれ違っても構いません。

演算子

定番の+ - * / %(余り)はありますが、pythonにはある累乗の**はありません。また、静的型付け言語なので以下のような整数同士の割り算は小数点以下が切り捨てられます。どちらかをdoubleにキャストしましょう。

int a = 10;
int b = 3;
int result = a / b; // 3
double result = (double) a / b // 3.333...

また、自分自身に+ - * / %を行う+= -= *= /= %=や、自分自身に1だけ足したりひいたりする++ -- などもあります。

int i = 0;
i += 1;    //これは
i = i + 1; //これと同等

i++;       //これは
i = i + 1; //これと同等

比較演算子として< <= > >= == !=があります。これらは前後のオブジェクトを比較して真偽値(bool)を返します。<= >=は数学における≦ ≧に相当し、== !=は数学における= ≠に相当します。また、&& ||はそれぞれ真偽値における論理積・論理和をあらわし、!は真偽値の直前につけることで真偽値を反転させることができます。

※論理積 二つともがtrueのときtrueを返す
※論理和 二つのうち一方がtrueのときtrueを返す。

ifとかforとか

条件分岐

C言語と違い条件式に指定できるのは真偽値だけです。ifステートメントとswitchステートメントが実装されており、詳しくはMicrosoft公式に書いてあります。

int x = 10;
if(x < 0)
{
  //ここに処理を書く
}
else if (x < 10) { /* else ifを使うことでいくつものパターンに対応できる */ }
else
  return 0; // 終わるときはelseで終わる。1文で終わるなら 波カッコ{} は不要

switch(x) //たくさんの条件で分岐したいときに使う
{
    case 10: // xが10の時の処理
        break;
    case 20: // xが20の時の処理
        break;
    default: // xが上のすべてと異なったときの処理
        break;
}

繰り返し

C#には繰り返しの処理としてforステートメント, foreachステートメント, whileステートメント, do-whileステートメントが実装されています。Microsoft公式を見るとここより詳しい情報を得られます。

for(var i = 0; i < 100; i++) // (初期化処理 ; 終了条件 ; ループが終わるごとに実行される)
{
    // 処理を書く
}

var arr = new int[]{ 0, 1, 2, 3, 4 };
foreach(var item in arr) //コレクションのループ
{
    item++;
}
foreach((var item, var i) in arr.Select((num, index) => (num, index))) //インデックス付きのコレクションのループ
{
    item += i;
}

while(true)
{
   // 条件を満たす間繰り返す。
}
do
{
  // 最初の一回実行され後条件を満たす間繰り返す。
}while(true)

デリゲート

C#では関数オブジェクトを格納するものとしてdelegateというもがあり、下のように定義し、使うことが可能です。

public delegate string Hoge(int x);//引数がint型 返り値がstring型の関数型Hoge

public class Program
{
  public static void Main()
  {
    Hoge func = new Hoge(Foo); //クラスをインスタンス化するときみたいにかける。
    Hoge func = Foo; //こうもかける。
  }

  public static string Foo(int x)
  {
    return x.ToString();
  }
}

このように定義できますが、実際はFuncActionを使うことが多いです。この2つはジェネリクスを用いて(厳密には違いますが)下のように定義されており、どんな型でもその場で指定して入れることができるので大変便利です。

public delegate void Action<T1>(T1 arg); //Actionは返り値がvoid
public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2); //Funcは返り値がTResult(最後の型)

//使い方
public class Program
{
  public static void Main()
  {
    Func<int,int,int,string> func = Foo; //こんな感じで書けます。
  }

  public static string Foo(int x, int y, int z)
  {
    return x.ToString();
  }
}

コレクション

コレクションはデータをまとめて扱うためのクラスでジェネリクスなしのものはSystem.Collection名前空間に、ジェネリクスありのものはSystem.Collection.Generics名前空間にあります。代表的なコレクションはリストと辞書型があります。

リスト

リストは可変長配列のようなもので、配列とは違い検索メソッドやソートメソッドが実装されてたりします。

var arr = new List<int>();
arr.Add(3); //Addで追加
var temp = arr[0]; //これで中身を見る
arr.Remove(3); //引数と最初に一致したものを削除

詳細はMicrosoft公式で確認してください。

辞書型

辞書型はキーと値をセットで保持することができるコレクションです。

var dic = new Dictionary<string, string>();
dic.Add("key", "value"); //Addで追加
var temp = dic["key"]; //これで取得
dic.Remove("key"); //引数と一致したキーを持つ値を削除

詳細はMicrosoft公式で確認してください。

プロパティ

プロパティはclassやstructのメソッドをあたかもフィールドであるかのように扱う機能です。

public class Hoge
{
    public int Number
    {
        get
        {
            return _number;
        }
        private set // アクセス修飾子を片方だけ変えることが可能
        {
            if(value < 0) // 入ってきた値は valueキーワード に格納されている
                value = 0;
            _number = value;
        }
    }
    private _number;

    public int Id { get; private set; }  // getとsetだけ書いて中身は書かないとコンパイル時に
    public int Id { get{ return _id; } set{ _id = value; } }  // これみたいに展開される
    private int _id;

    public void HogeHoge()
    {
        Number = 10; //setプロパティが呼ばれる
    }
}

フィールドではなくプロパティで実装することのメリットはset時に制約をつけたりget時にnullの場合などで条件分岐したりできることがあります。また、interfaceにはフィールドは定義できないですがプロパティは実際は関数なので定義できます。

名前空間

C#ではコードを名前空間で分割することができす。下のようにクラスやメソッドと同じように宣言することができます。名前空間では同じ名前空間にあるもの、またはusingを使って宣言したものだけが制限なく使えます。別の名前空間のものを使用する際はA.Hogeのように名前空間から指定するかusing A;という文言を書いておく必要があります。

using System;
namespace A
{
   public class Hoge
   { }
}

yield return

yield returnで値を返すことで簡単にIEnumeratorを作ることができます。returnの代わりにyield returnを使うとIEnumeratorが自動的に生成され、foreachステートメントで使用できます。

public static void Main()
{
    foreach(var i in GetEnumerator())
    {
        // 処理
    }
}

private static IEnumerator GetEnumerator()
{
    int i = 0;
    while(true)
    {
        yield return i++;
    }
}

UnityではCoroutineという機能で使わています。

継承

C#はオブジェクト指向の言語なので継承が実装されています。継承とはあるクラスから性質を受け継いだ新しいクラスことで、派生とも呼ばれています。例としては以下のようなものがあります。

class Person
{
  public string name; // 名前
  public int    age;  // 年齢
}

class Student : Person
{
  public int    id;   // 学籍番号
}

継承 - C# によるプログラミング入門 | ++C++; // 未確認飛行 Cより

時間がなかったので継承はあとで書きます。

参考させていただいたサイト