20200531のUnityに関する記事は4件です。

waidayoを使って手軽にUnityやUE4でフェイシャルキャプチャ(表情取得)しよう

はじめに

こんなツールを作りました。
Oredayo4V (おれだよ for VTuber)です。

私は、iOSのアバターフェイシャルキャプチャアプリ「waidayo」の作者ではありません。
赤の他人です。データや通信仕様の解析や問い合わせをしたわけでもありません。

  • ではなぜ、こういうものが作れたのか?
  • どういう仕組で連携できているのか?
  • 同様のものを作るにはどうすればいいのか?

について本記事では解説いたします。

VMCProtocol

突然ですが、皆さんはVirtual Motion Capture Protocol (VMCProtocol)はご存知でしょうか?

image.png
https://sh-akira.github.io/VirtualMotionCaptureProtocol/

バーチャルモーションキャプチャープロトコルの名前の通り、バーチャルモーションキャプチャーと通信するためのプロトコルです。
通信して何をするのかというと、キャプチャしたモーション情報を送受信します。

もともとはバーチャルモーションキャプチャーからUnityにモーションを送信するために作られたものでしたが、元より仕様は公開されており、誰でも利用することができます。

サンプルや、リファレンスとなる実装も公開されており、現在15以上のアプリケーションが対応しています。

これにより、アバターの姿勢や表情などのトラッキングと、描画、センシングをそれぞれ別々のアプリケーションで行うことができるようになります。
そう、ここでFace IDの表記がある通り、nmちゃん氏のwaidayoもこのVMCProtocolに対応したアプリケーションです。

image.png

具体的な実装

Oredayo4VはUnityで実装されています。
VMCProcolには代表的な2つのライブラリ実装があります。(どちらもMITライセンスです)

  • EVMC4U - Unity向けモーション受信アセット
  • VMC4UE - Unreal Engine向けモーション受信プラグイン

今回は、先に述べたとおり、Unityで実装したため、EVMC4Uを使いました。
EVMC4Uには、ボーン情報、表情その他の受信状機能があらかじめ備わっており、UnityPackageをインポートするだけでもう動きます。

あとはVRMを読み込み、waidayoの送信先をEVMC4Uのポートに設定するだけです。
概ねの実装はこれだけで、あとはカメラの移動やその他を制御する仕組み、UIを延々載せていっただけです。

WPFとの通信は、sh-akira氏のUnityMemoryMappedFile
ウィンドウ制御はsh-akira氏のVMCUnityWindowExtensions
仮想Webカメラはschellingb氏のUnityCaptureなど、
ひたすらライブラリを組み合わせていった結果です。

VMC4UEを使えば、UnrealEngine4版も簡単に作れることと思いますし、既存のゲームなどのプロジェクトにこれらライブラリを導入すれば、簡単に背景付きの撮影環境を構築することができます。

もちろん、ゲーム自体に組み込んでしまうこともできます。
(VR飛行ゲームPilotXrossという例があります)

ボーンの受信

EVMC4Uには様々な支援機能や最適化が含まれていますが、やっていることは単純です。
以下がVMCProtocol公式ページに記載のボーン・表情受信サンプルです。

waidayoから受信すればフェイシャルキャプチャになりますし、バーチャルモーションキャプチャーから受信すればフルボディトラッキングになります。単純ですね。

/*
 * SampleBonesReceive
 * gpsnmeajp
 * https://sh-akira.github.io/VirtualMotionCaptureProtocol/
 *
 * These codes are licensed under CC0.
 * http://creativecommons.org/publicdomain/zero/1.0/deed.ja
 */
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using VRM;

[RequireComponent(typeof(uOSC.uOscServer))]
public class SampleBonesReceive : MonoBehaviour
{
    public GameObject Model;
    private GameObject OldModel = null;

    Animator animator = null;
    VRMBlendShapeProxy blendShapeProxy = null;

    uOSC.uOscServer server;

    void Start()
    {
        server = GetComponent<uOSC.uOscServer>();
        server.onDataReceived.AddListener(OnDataReceived);
    }

    void Update()
    {
        if (blendShapeProxy == null)
        {
            blendShapeProxy = Model.GetComponent<VRMBlendShapeProxy>();
        }
    }

    void OnDataReceived(uOSC.Message message)
    {
        if (message.address == "/VMC/Ext/Root/Pos")
        {
            Vector3 pos = new Vector3((float)message.values[1], (float)message.values[2], (float)message.values[3]);
            Quaternion rot = new Quaternion((float)message.values[4], (float)message.values[5], (float)message.values[6], (float)message.values[7]);

            Model.transform.localPosition = pos;
            Model.transform.localRotation = rot;
        }

        else if (message.address == "/VMC/Ext/Bone/Pos")
        {
            //モデルが更新されたときのみ読み込み
            if (Model != null && OldModel != Model)
            {
                animator = Model.GetComponent<Animator>();
                blendShapeProxy = Model.GetComponent<VRMBlendShapeProxy>();
                OldModel = Model;
            }

            HumanBodyBones bone;
            if (Enum.TryParse<HumanBodyBones>((string)message.values[0], out bone))
            {
                if ((animator != null) && (bone != HumanBodyBones.LastBone))
                {
                    Vector3 pos = new Vector3((float)message.values[1], (float)message.values[2], (float)message.values[3]);
                    Quaternion rot = new Quaternion((float)message.values[4], (float)message.values[5], (float)message.values[6], (float)message.values[7]);

                    var t = animator.GetBoneTransform(bone);
                    if (t != null)
                    {
                        t.localPosition = pos;
                        t.localRotation = rot;
                    }
                }
            }
        }
        else if (message.address == "/VMC/Ext/Blend/Val")
        {
            string BlendName = (string)message.values[0];
            float BlendValue = (float)message.values[1];

            blendShapeProxy.AccumulateValue(BlendName, BlendValue);
        }
        else if (message.address == "/VMC/Ext/Blend/Apply")
        {
            blendShapeProxy.Apply();
        }
    }
}

おわりに

VMCProtocolを使用すると、非常に安価に、かつ様々な資産を活用してソフトウェアや撮影環境を構築できます。
ぜひお試しください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Unity] Scale With Screen Size / Expand のCanvasのUIと同じ大きさに見えるようにObjectをScaleさせる

var referenceWidth = 1334;
var referenceHeight = 750;

var cam = Camera.main;

var resolutionAspect = (float)referenceWidth / referenceHeight;
var camAspect = cam.aspect;

var fov = cam.fieldOfView * Mathf.Deg2Rad * 0.5f;
if (resolutionAspect > camAspect)
{
    var hTan = Mathf.Tan(fov) / (1 / camAspect * resolutionAspect);
    fov = Mathf.Atan(hTan);
}

var distance = Vector3.Distance(transform.position, cam.transform.position);     
transform.localScale = Mathf.Sin(fov) * distance * Vector3.one;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityからAndroid端末にアプリをインストールするときにでたエラーについて

エラー

adb: failed to install /Users/UserName/UnityProject/TestUnityProject/ProjectName/Builds/Apk/AppName.apk: Failure [INSTALL_FAILED_OLDER_SDK: Failed parse during installPackageLI: /data/app/vmdl1509473597.tmp/base.apk (at Binary XML file line #8): Requires newer sdk version #28 (current version is #27)]
```

日本語訳

adb:/Users/UserName/UnityProject/TestUnityProject/ProjectName/Builds/Apk/AppName.apk:のインストールに失敗しました:[INSTALL_FAILED_OLDER_SDK:installPackageLIの解析に失敗しました:/data/app/vmdl1509473597.tmp/base.apk(バイナリXMLファイル)行#8):新しいSDKバージョン#28が必要(現在のバージョンは#27)]

解決策

「インストール先のOSのバージョン」と「Unityで吐き出すApiLevel」を合わせる


SDKがおかしいのかと思い、アップデートなどをやりまくったが、解決せず、、
エラー文を検索したら、こちらの記事が出てきて、バージョンが合ってないということがわかりました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】鏡に映るは鏡の世界なり

概要

タイトル訳:Unityで鏡を作りたい!
今回紹介する考え方は応用が利くので、何かのお役に立てばと思います。

動作環境

  • Unity 2019.3.14f1

原理

まず初めに、鏡に映るものをどうやってレンダリングするかを考えましょう。

発想

鏡の映像を撮りたいということで、
カメラに鏡の世界を撮ってきてもらいましょう!

※アニメーション中の影の動きは気にしないでください
※青暗い面は鏡の境界面を表現しています。

こちらのイメージ映像の通りに世界を反対にしましょう。
ここで重要なのは「鏡の面を境にして世界を反対にする」ことです。

実装

実現方法

鏡の境界を中心として、鏡面方向のスケールを-1にします。
ということで 射影変換行列 を使いましょう!

変換の順番は

  1. 鏡の位置を原点とする座標系に変位(鏡のローカル座標に変換)
  2. 原点を中心として、Z方向のスケールを-1に変換
  3. 1.の逆変位
  4. 3.の後にできた世界をそのままレンダリング (通常のカメラ座標への変換)

上記変換を実施する行列をカメラのViewMatrixより先に実施されるように差し込んであげれば良いです。

以下はイメージです。表示している軸はワールド座標系の軸です。

この動画の通りカメラの位置や回転は変更する必要はありません。
世界だけ鏡の境界で反対にした後に映像を撮れば鏡の映像になります。

鏡用カメラのCulling Mask

新たにMirrorレイヤーを追加して、鏡用カメラのCulling Maskからは除外します。
鏡オブジェクトのレイヤーをMirrorにして、鏡用カメラにレンダリングされないようにしましょう。

変換を実施するコード

Unityのベクトルは列オーダーなので、先に実施させたい場合は後側に行列を挿入することになります。

mirrorCamera.worldToCameraMatrix =
    mainCamera.worldToCameraMatrix *  // 通常のカメラ座標への位置 (4. の変換)
    mirror.localToWorldMatrix *       // 3. の変換
    Matrix4x4.Scale(new Vector3(1, 1, -1)) *  // 2. の変換
    mirror.worldToLocalMatrix;        // 1. の変換

ここまでの結果

早速、以下のシーンで鏡用カメラの映像を見てみましょう!

こうなります。。。

何が起こっているというと、モデルのポリゴンの向きが反対になってるんですね。

TransformのScaleが-1の場合は、Unityが勝手にポリゴンの向きを逆してレンダリングしてくれますが、
カメラのViewMatrixでの反対化はこちらで逆にしてやる必要があります。

こちらの記事 もどうぞ! スケールとポリゴンの向きについて語っています!

Frontface Cullingに切り替える

以下のメソッドをコールすればBackface CullingからFrontface Cullingに切り替わります。

GL.invertCulling = true;

注意点として、カメラの描画完了後は完了前の状態に戻してあげてください。
MainCameraなど通常のカメラまでポリゴンが反対になります。

bool oldCulling = GL.invertCulling;
GL.invertCulling = true;  // Frontface Cullingに切り替える

mirrorCamera.Render();  // レンダリングの実施

GL.invertCulling = oldCulling;  // 切り替え前に戻す

問題なく鏡の世界が描画されました。影の向きも鏡の世界のものになっていますね。

鏡の映像を鏡に反映

RenderTextureやDepthMaskを使って鏡に反映してください。
サンプルシーンを記事の最後に載せておきます。


違和感なさそうですね。

DepthMask方式が使用可能な理由

※DepthMaskについてはこちらの記事をご参照ください。

これは鏡の世界を映すカメラが、通常のカメラと同じ位置にあるからです。
よって鏡の枠内だけ残して通常世界を描画すれば、鏡を表現できます。

ScreenSpaceMappingについて

RenderTextureを使用する場合はScreenSpaceMapping用のシェーダを用意する必要があります。
tex2Dprojを使う必要があったりと少し特殊なシェーダになります。
解説されてる記事はたくさんありますので、詳しく知りたい方はググってみて下さい!

最適化

不要なときはレンダリングをスキップ

鏡も水面反射などと同様に、MainCameraに反射オブジェクトが映らないときはレンダリング処理自体が無駄になるので、
Renderer + OnWillRenderObjectなどと組み合わせて、映らないときはスキップするようにしてあげてください。
※OnWillRenderObjectのマニュアルページに書いてあるWater.csが参考になります。

注意点
実はこのRenderer + OnWillRenderObjectを使用する方法ですが、UWPとHDRPではできません。。。
UWPはCamera.Renderが、HDRPではOnWillRenderObjectがサポートされていません。
よって、このUWPもしくはHDRPで実施する場合はそれぞれの代替手段を考える必要があります。
ご留意ください。

鏡の枠内のみ描画する

実装の敷居は高くなりますが、Unityの安原様が書かれた記事が参考になると思います。
枠外の描画をどうしても避けたい方向けです。

[Unity]斜め(Oblique)の投影行列で空間を切り取る

おまけ

「発想」のところで記載した動画の様にアニメーションしながら世界を反転させる方法

鏡用カメラに使用した変換行列を以下のコードの様に、
Z方向のスケールを変換する行列をフレームごとに遷移させれば実現できます。

float value = curve.Evaluate(Time.time * bias) * 2 - 1;
Matrix4x4 zReverseMatrix = Matrix4x4.Scale(new Vector3(1, 1, value));  // Zのスケールをフレーム毎に遷移
GL.invertCulling = value < 0;  // ZScaleが反対になったらFrontface Cullingに切り替え

mirrorCamera.worldToCameraMatrix =
    mainCamera.worldToCameraMatrix *  // 通常のカメラ座標への位置 (4. の変換)
    mirror.localToWorldMatrix *       // 3. の変換
    zReverseMatrix *  // 2. の変換
    mirror.worldToLocalMatrix;        // 1. の変換

ちなみにメインカメラから見るとこんな感じになります

サンプルシーン

雑感

本当はVRでも実現したかったんですが、Unity2019.3.14f1もしくは2020.1.0b10でCamera.SetStereoViewMatrixを使うとおかしくなるため一旦保留にしました。
一応UnityにBug Reportとして出しましたが、対応はかなり時間がかかる気がします(実は1年前にも似たようなバグ報告をしたんですけど、対応されませんでした)。
やるとしたらカメラ2つを使うやり方になるかと思います。そのうち記事にしようと思います。

あと、UWPやHDRPでサポートされなくなるメソッドたちがあったりして、
使用するパイプラインごとにいろいろ変更しないといけないのが面倒かなって思います。

今回の記事の考え方を通して、皆さんの新しい発想の手助けになればと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む