MRが楽しい

MRやVRについて学習したことを書き残す

Blenderで作成した3DモデルをMMDに取り込む

本日はMMDの学習枠です。
私事ですが、今週の金曜日、会社の勉強会でUnityのアニメーションについて簡単な講座を行う事となりました。
アニメーションについては一度記事を書いていますが、今週は改めて、Unityのアニメーション設定をまとめていきます。
bluebirdofoz.hatenablog.com

そして、それに当たり。折角ですから3Dモデルを自由に操作できるようになる楽しみが伝わるような講座にしたいと考えました。

資料の表紙に、3DモデルがPC操作を行っているCGを使うなんてどうでしょう。
早速、取り掛かりました。

まずはCGで使うノートパソコンの3Dモデルをblenderで作成します。探せばフリーでありそうですが、3Dモデリングの復習も兼ねて自作です。
f:id:bluebirdofoz:20170725024112j:plain
今回は背面しか見せないので、ディスプレイやキーボードは作り込んでいません。

さて、このままblenderで画像を出力すれば良いんですが、折角の表紙CGです。高精細なMMDモデルも使いたくなりました。
しかし、MMDモデルはblender上では表情のモーフが使えないという問題に突き当たりました。
github.com
現状、blenderのアドオンであるmmd_toolsは頂点モーフにしか対応しておらず、MMDモデルの表情豊かなモーフが使えません。
これは勿体ない。

ですので、blenderで作成した3DモデルをMMD側に持っていき、MMD上で3Dモデルを撮影します。
これならばMMDモデルのモーフが利用可能です。

MMDではキャラクタ以外のオブジェクトを「アクセサリ」と表現しており、これらは x 形式のファイルで取り込めます。
作成したノートパソコンをblenderで x 形式のファイルでエクスポートします。
f:id:bluebirdofoz:20170725024301j:plain

エクスポート形式にxファイルが存在しない場合は以下を参考にユーザ設定を変更してください。
・blender2.62でxファイルをエクスポートするのメモ
 http://blog.livedoor.jp/hawamo21/archives/3997789.html

xファイルに変更すればMMDに3Dモデル取り込めます。
f:id:bluebirdofoz:20170725024330j:plain

このとき、xファイルを読み込むと、3Dモデルにテクスチャファイルが反映されていない場合があります。
xファイルの中身をテキストファイルで確認して、指定の相対パスにテクスチャファイルが存在するか確認してみてください。
デフォルトだと、xファイルと同じディレクトリにテクスチャファイルが存在する必要があります。

ノートパソコンを操作しているMMDキャラクタ達を配置します。MMD上なので様々な表情が簡単に付けられます。
f:id:bluebirdofoz:20170725024436j:plain
表示->出力サイズ で静止画の出力サイズを設定して、ファイル->画像ファイルに出力 で画像を出力できます。

後は出力した画像をペイントでちょちょいとイジれば……。
f:id:bluebirdofoz:20170725024516j:plain
3Dモデルを使った表紙CGの出来上がりです!

はい。見事に、表紙の作成のみで一日を費やしてしまいました。
明日からはちゃんとUnityのアニメーション設定のまとめに着手します。

Blenderで3Dモデルに法線マップを適応する

3Dモデリングの勉強枠です。
さて前回作成したhololensの3Dモデルですが、細分割曲面モディファイアを取っ払うと以下のような見た目になります。
f:id:bluebirdofoz:20170724015158j:plain

光の反射に注目するとよく分かりますが、カックカクですね。しかし、これでも500ポリゴンあるモデルなのです。
hololensアプリで利用する小物オブジェクトとしてはこれ以上ポリゴン数は増やしたくありません。

そこで今回は「法線マップ」を利用し、少ないポリゴン数で3Dモデルを滑らかに見せる方法をトライします。
今回、面法線の手動変更は行わないので、どちらかというと「バンプマップ」という表現が正しいかもしれません。
・Bump and Normal Maps
 https://wiki.blender.org/index.php/Doc:JA/2.6/Manual/Textures/Influence/Material/Bump_and_Normal
docs.unity3d.com
3d-memo.blog.jp

まずは法線マップ用の画像を作成します。
f:id:bluebirdofoz:20170724015343j:plain

次にベイク用に作成対象のオブジェクトを複製します。
このオブジェクトで高精細なポリゴンを作成し、法線マップの元とします。
f:id:bluebirdofoz:20170724015353j:plain
元のオブジェクトは邪魔にならないよう、非表示にしておきます。

多重解像度モディファイアを適用してポリゴンの解像度を上げます。
f:id:bluebirdofoz:20170724015407j:plain
今回は4万ポリゴンほどまであげてみました。

さて、法線マップを作成します。
以下の二つの操作が必要です。
・プロパティエディタの[シーン]タブから、ベイクの設定を行う。
  ベイクモード: ノーマル
  法線空間: タンジェント
  「選択 → アクティブ」チェック
・スカルプトしたオブジェクトを選択状態で、Shiftキーを押しながら、元のオブジェクトをクリックする。
f:id:bluebirdofoz:20170724015416j:plain
この状態で「ベイク」ボタンを押下します。
※ 参照記事では元のオブジェクトは非表示のままでも問題ないとありましたが、私の環境だと表示する必要がありました。

先ほど作成した画像に解像度をあげたオブジェクトを元にした法線マップが作成されますので保存します。
f:id:bluebirdofoz:20170724015426j:plain

作成した画像をノーマルマップとして元のオブジェクトの各マテリアルに設定すればOKです。
f:id:bluebirdofoz:20170724015440j:plain
画像は一方の設定のみキャプチャしていますが、例えば、このhololensオブジェクトだと本体部とグラス部は別々のマテリアルなので両方に反映が必要です。

レンダー画面で確認してみます。ポリゴン数500のままで滑らかな表現になりました。
f:id:bluebirdofoz:20170724015449j:plain
トップ画像の反射と比較すると、その効果がよく分かります。また前回、利用した以下の9千ポリゴンのモデルと比べても遜色ありません。
f:id:bluebirdofoz:20170724015513j:plain
パッと見にはポリゴン数に20倍近い差があるとは分からないでしょう。

むしろ本体部分のカーブなど箇所によっては法線マップを利用したものの方が滑らかにも見えます。
逆にグラス部分は法線マップの方はポリゴンの切れ目が見えます。透過部分の光の屈折設定はポリゴン基準だからと思われます。
この辺りの法線マップの適切な活用法については今後も学んでいきたいです。

さて、この手法はポリゴンとテクスチャが密接に対応しています。
このため、この方法を用いると細分割曲面モディファイアによるポリゴン数の調整が上手くできなくなります。
(法線マップを無効化する必要が出てきます)

万単位のポリゴンでも十分な描画が可能な高性能PCが普及した現状では、この法線マップでポリゴンの高精細化を計るよりもモディファイアでポリゴン数を増やした方が流用性の面では有効と思われます。

しかし、hololensのようなPCのパワーが足りない機器では利用価値のある技術です。
hololensで高精細な3Dグラフィックを売りにしたアプリを作成する際には非常に役立つでしょう。今後も活用していきます。

Blenderで透過のマテリアルを設定する

3Dモデリング勉強枠です。
前回、hololensの3Dモデルを作成しましたが、このモデルはグラス部分が透過されておらず、顔が見えませんでした。
f:id:bluebirdofoz:20170723025404j:plain

bluebirdofoz.hatenablog.com

今回は透過するマテリアルの設定方法を調べてみます。
・マテリアルの透過
 http://cg.xyamu.net/Blender/entry34.html

もっとも簡単なのはテクスチャファイル(png)に直接、透過情報を入れ込むことです。
これならばテクスチャファイルの編集だけで対処可能です。
f:id:bluebirdofoz:20170723025449j:plain

しかし、この方法では透過量を調整するたびにテクスチャファイルを編集する必要があります。
マテリアルの設定として調整できるようにしたいので、そちらの方法も試してみます。

まずは頂点ごとにマテリアルを割り当て、レンズ部分と本体部分のマテリアルを切り離します。
qiita.com

マテリアルを分けてしまえば、以下の個別設定が行えます。
・マテリアルの透過
 http://cg.xyamu.net/Blender/entry34.html

透過の設定だけでなく、スペキュラ(鏡面反射光)の調整なども可能です。
f:id:bluebirdofoz:20170723025529j:plain
少しキツめに反射している感じに仕上げました。

前回の写真を透過反映後のhololensに差し替えて撮ってみます。
f:id:bluebirdofoz:20170723025548j:plain
グラス部分が透過して、キャラクタの瞳などが見えるようになっています。いい感じです。

Blenderでhololensの3Dモデルを作る

本日から3Dモデリングの学習も強化していきます。
f:id:bluebirdofoz:20170722191555j:plain

調査の結果、hololensアプリの3Dモデルにはそれほど高ポリゴンが要求されないことが分かりました。
良い機会なのである程度、3Dモデルを自作できるスキルを習得します。
bluebirdofoz.hatenablog.com

今回は無機物のモデリングに挑戦しました。
今後、プロジェクト等で一番使いそうだという理由からhololensの3Dモデルを作成です。

インターネットにある画像から三面図を作成しました。添付しておきます。
f:id:bluebirdofoz:20170722191622j:plain

真横からの画像が見つからず、横向きの画像が少しズレていることに注意してください。
枠の太さの目安にはなると思います。

今回のモデル作成で利用した技術は全て、以下の記事で学習した内容から流用しています。
bluebirdofoz.hatenablog.com

ポリゴン数が約500程度で作成完了。抑えたつもりでもこれくらいとは……。
大きな画面で見ると、カクカクなので細分割曲面モディファイアを利用したものを貼りつけ。
f:id:bluebirdofoz:20170722191833j:plain
しかしベースはあくまで500ポリゴンのモデルなので適正ポリゴン数に合わせた修正も容易です。

hololensで利用するためには、グラス部分を含め、少し彩度を高めに設定しています(黒色はhololensで見えないため)。
今後、hololensアプリに適用しつつ適正な彩度についても調べていきたいと思います。

当然、blender内で流用できます。blenderに取り込んだMMDモデルに被せて写真を撮ってみました。
f:id:bluebirdofoz:20170722191555j:plain
MMDキャラがhololensをつけると益々ゴツい。

hololensでMMDモデルにダンスを踊ってもらった

MMDモデルのダンス枠の続きです。
f:id:bluebirdofoz:20170721012037g:plain
前回、hololensでMMDモデルに踊ってもらうにはポリゴン数を大きく削減する必要があることが分かりました。
bluebirdofoz.hatenablog.com

では、この問題をどう解決するか?の結論を一つ出しておきたいと思います。

今回はモデルのポリゴン数削減の改造を行うため、MMDに付属する標準モデルを利用します。
・VPVP
 http://www.geocities.jp/higuchuu4/

標準モデルはそのままだと2万ポリゴンです。
f:id:bluebirdofoz:20170721011619j:plain
今回はモデル単体のプロジェクトなので4千ポリゴンまでの削減とします。
ダンスモーションを設定してfbxファイルで出力します。

このとき、削減方法に拘るのであれば、ポリゴンを削減しても目立たない箇所を削減すべきです。
顔のポリゴンよりも、体のポリゴン削減は多少強く行っても目立たないでしょう。
モデリングが可能ならば、手を握りこぶしの形に修正したりするのも良いと思います。

ポリゴン削減を行ったモデルをUnityプロジェクトに取り込みます。
f:id:bluebirdofoz:20170721011743j:plain

さて、今回のポリゴン数削減に対する一つの回答ですが、サイズを小さくするということがあげられます。
3Dモデルがフィギュアくらいの大きさになれば、ポリゴン数が十分の一になっても気にならないはず……という理屈です。
f:id:bluebirdofoz:20170721011835j:plain
現実世界比で凡そ身長10cmのミニ初音さんです。

またSpatialMappingのポリゴン数も削減しておきます。
デフォルトでは範囲が10mですが、そんなにも要らないので、2mにしておきます。
f:id:bluebirdofoz:20170721011859j:plain
メッシュも見えると目立つと思わるため、Occlusionにしています。

アプリを起動して確認します。
f:id:bluebirdofoz:20170721012037g:plain

録画していてもしっかりダンスしてくれました。
hololensの狭めの視野内にモデルの全体像が収まってダンスするので、その点でもモデルを小さくするという解決法は悪くありません。

ただ、遠目で解像度も低い動画では違和感はありませんが、やはり実際に近寄って見るとポリゴンの粗さが目立ちます。
また、モデルが小さいため、SpatialMappingの更新で地面の高さに誤差が生じると、モデルが大きく上下してしまいます。
魅せ方やプロジェクトの構成に、もう少し工夫が必要ですね。

公式チュートリアル「HOLOGRAMS 230 5章」を試してみる

本日はチュートリアルお試し枠です。
いつも通り、以下ブログの記事を参考に実施します。
azure-recipe.kc-cloud.jp

今回は空間マッピングによるオクルージョンを確認します。
記事の通りアプリを修正してプログラムを実行してみます。

前回同様、カメラ近くの床に地球のオブジェクトが表示されます。
f:id:bluebirdofoz:20170720034158j:plain

これを壁を通して見てみると……。
f:id:bluebirdofoz:20170720034209j:plain
青色の地球の輪郭が透過して見えるようになります。

また、他の惑星は以下のように格子状のメッシュで表示されます。
f:id:bluebirdofoz:20170720034216j:plain

さて今回追加したコードは以下のコードです。
・PlanetOcclusion.cs

    // Update is called once per frame
    void Update()
    {
        /* TODO: 5.a DEVELOPER CODING EXERCISE 5.a */

        // Check to see if any of the planet's boundary points are occluded.
        for (int i = 0; i < checkPoints.Length; i++)
        {
            // 5.a: Convert the current checkPoint to world coordinates.
            // Call gameObject.transform.TransformPoint(checkPoints[i]).
            // Assign the result to a new Vector3 variable called 'checkPt'.
            Vector3 checkPt = gameObject.transform.TransformPoint(checkPoints[i]);

            // 5.a: Call Vector3.Distance() to calculate the distance
            // between the Main Camera's position and 'checkPt'.
            // Assign the result to a new float variable called 'distance'.
            float distance = Vector3.Distance(Camera.main.transform.position, checkPt);

            // 5.a: Take 'checkPt' and subtract the Main Camera's position from it.
            // Assign the result to a new Vector3 variable called 'direction'.
            Vector3 direction = checkPt - Camera.main.transform.position;

            // Used to indicate if the call to Physics.Raycast() was successful.
            bool raycastHit = false;

            // 5.a: Check if the planet is occluded by a spatial mapping surface.
            // Call Physics.Raycast() with the following arguments:
            // - Pass in the Main Camera's position as the origin.
            // - Pass in 'direction' for the direction.
            // - Pass in 'distance' for the maxDistance.
            // - Pass in SpatialMappingManager.Instance.LayerMask as layerMask.
            // Assign the result to 'raycastHit'.
            raycastHit = Physics.Raycast(Camera.main.transform.position, direction, distance, SpatialMappingManager.Instance.LayerMask);

            if (raycastHit)
            {
                // 5.a: Our raycast hit a surface, so the planet is occluded.
                // Set the occlusionObject to active.
                occlusionObject.SetActive(true);

                // At least one point is occluded, so break from the loop.
                break;
            }
            else
            {
                // 5.a: The Raycast did not hit, so the planet is not occluded.
                // Deactivate the occlusionObject.
                occlusionObject.SetActive(false);
            }
        }
    }

そもそも、このPlanetOcclusion.csが何処にアタッチされているかですが、各惑星のオブジェクト毎にアタッチされています。
動きとしてはカメラとチェックポイントの相対的な距離と方向を計算し、その直線状にSpatioalMappingの障害物があれば格子オブジェクトを有効化しています。

一方、Earthに割り当てたシェーダの輪郭表示部分は以下です。
・OccusionRim.shader

    struct v2f
    {
     float4 viewPos : SV_POSITION;
     float2 uv : TEXCOORD0;
     float3 normal : TEXCOORD1;
     float3 viewDir : TEXCOORD2;
    };

    v2f vert(appdata_tan v)
    {
     v2f o;
     o.viewPos = UnityObjectToClipPos(v.vertex);
     o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                    o.normal = UnityObjectToWorldNormal(v.normal);
     o.viewDir = normalize(WorldSpaceViewDir(v.vertex));
     return o;
    }

    half4 frag(v2f i) : COLOR
    {
     half Rim = 1 - saturate(dot(normalize(i.viewDir), i.normal));
     half4 RimOut = _RimColor * pow(Rim, _RimPower);
     return RimOut;
    }
    ENDCG

オブジェクトの法線方向とカメラへのワールド方向の内積を求め、反転させることで輪郭部分を抽出しているようです。
www.opengl-tutorial.org
docs.unity3d.com
saturate (DirectX HLSL)

公式チュートリアル「HOLOGRAMS 230 4章」を試してみる

本日はチュートリアルお試し枠です。
いつも通り、以下ブログの記事を参考に実施します。
azure-recipe.kc-cloud.jp

今回はSpatialMappingで面の判定を確認します。
記事の通りアプリを修正してプログラムを実行してみます。

一点、記事中に書いてあるPlaySpaceManager.csはおそらくPlaceable.csの誤りなので注意が必要です。
公式ページを確認しました。
・Holograms 230
 https://developer.microsoft.com/en-us/windows/mixed-reality/holograms_230

アプリを起動します。逆向きではありますが壁面にポスターが表示されました。
f:id:bluebirdofoz:20170719021554j:plain
ポスターをタップすると、ドラッグできます。
このとき、平面でないところにポスターを持ってくると、置けないことを意味する赤色の背景色が表示されます。
f:id:bluebirdofoz:20170719021602j:plain
配置可能な平面だと、白色の背景色になります。
f:id:bluebirdofoz:20170719021617j:plain
タップするとポスターを配置できました。
f:id:bluebirdofoz:20170719021627j:plain

コードを確認します。
今回は、配置可能な平面かどうかを確認している関数を抽出しました。
・Placeable.cs

    // Threshold (the closer to 0, the stricter the standard) used to determine if a surface is flat.
    private float distanceThreshold = 0.02f;
(略)
    /// <summary>
    /// Verify whether or not the object can be placed.
    /// </summary>
    /// <param name="position">
    /// The target position on the surface.
    /// </param>
    /// <param name="surfaceNormal">
    /// The normal of the surface on which the object is to be placed.
    /// </param>
    /// <returns>
    /// True if the target position is valid for placing the object, otherwise false.
    /// </returns>
    private bool ValidatePlacement(out Vector3 position, out Vector3 surfaceNormal)
    {
        Vector3 raycastDirection = gameObject.transform.forward;

        if (PlacementSurface == PlacementSurfaces.Horizontal)        
        {
            // Placing on horizontal surfaces.
            // Raycast from the bottom face of the box collider.
            raycastDirection = -(Vector3.up);
        }

        // Initialize out parameters.
        position = Vector3.zero;
        surfaceNormal = Vector3.zero;

        Vector3[] facePoints = GetColliderFacePoints();

        // The origin points we receive are in local space and we 
        // need to raycast in world space.
        for (int i = 0; i < facePoints.Length; i++)
        {
            facePoints[i] = gameObject.transform.TransformVector(facePoints[i]) + gameObject.transform.position;
        }

        // Cast a ray from the center of the box collider face to the surface.
        RaycastHit centerHit;
        if (!Physics.Raycast(facePoints[0],
                        raycastDirection,
                        out centerHit,
                        maximumPlacementDistance,
                        SpatialMappingManager.Instance.LayerMask))
        {
            // If the ray failed to hit the surface, we are done.
            return false;
        }

        // We have found a surface.  Set position and surfaceNormal.
        position = centerHit.point;
        surfaceNormal = centerHit.normal;

        // Cast a ray from the corners of the box collider face to the surface.
        for (int i = 1; i < facePoints.Length; i++)
        {
            RaycastHit hitInfo;
            if (Physics.Raycast(facePoints[i],
                                raycastDirection,
                                out hitInfo,
                                maximumPlacementDistance,
                                SpatialMappingManager.Instance.LayerMask))
            {
                // To be a valid placement location, each of the corners must have a similar
                // enough distance to the surface as the center point
                if (!IsEquivalentDistance(centerHit.distance, hitInfo.distance))
                {
                    return false;
                }
            }
            else
            {
                // The raycast failed to intersect with the target layer.
                return false;
            }
        }

        return true;
    }
(略)
    /// <summary>
    /// Determines if two distance values should be considered equivalent. 
    /// </summary>
    /// <param name="d1">
    /// Distance to compare.
    /// </param>
    /// <param name="d2">
    /// Distance to compare.
    /// </param>
    /// <returns>
    /// True if the distances are within the desired tolerance, otherwise false.
    /// </returns>
    private bool IsEquivalentDistance(float d1, float d2)
    {
        float dist = Mathf.Abs(d1 - d2);
        return (dist <= distanceThreshold);
    }

ボックスコライダーの各コーナーからレイキャストの衝突判定を実施し、その距離の差が0.02f以下かどうかを判定しているようです。