20201128のUnityに関する記事は8件です。

Unityから音が出ない時の解決方法

すごく単純ですが、Unityから音が出ない時の解決方法です。

MuteAudioがオンで音が再生されない2_trim.png

「Mute Audio」が有効になっていると、Unityから音がでなくなります。
Projectビューから.mp3ファイルなどを選択し、ヒエラルキービューで再生ボタンを押すと再生されるので、スクリプトが原因かな?と勘違いしたりして、地味に問題に気付くまでに時間がかかりました。

これが有効な際にスクリプトでログを表示したら便利かなと思ったのですが、スクリプトで制御する方法はざっと調べたところなさそうでした。

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

Unityで可読性の高いアプリケーション開発をする方法

はじめに

Unityは自由度が高いため、人によってヒエラルキー構成であったり、スクリプトの実装方法などに大きなばらつきが生じます。
これにより、誰かが作ったUnityプロジェクトを見る際に、処理のフローが掴みづらく、この機能はなんで動いているんだろう?といった疑問を持った経験がある方は少なくないかと思います。
この問題を解決すべく、どのようなフローで処理が行われているかを把握しやすくした、普段私が実際に行っているUnityでのアプリケーション開発手法についてご紹介します。

まず結論

  • エントリーポイントを定義する
  • ヒエラルキー構成を階層化し責務を明示化する
  • エントリーポイントを定義したMainスクリプトで処理をハンドリングする

なぜわかりにくいのか?

Unityの処理のフローが分かりづらい要因として、エントリーポイントが明示的にないことが挙げられます。
例えば、至るスクリプトでStart()が記述されている場合、Start()の実行順序はランダムであるため、どのスクリプトのStart()から呼ばれるかは保証されません。
そのため、スクリプトからはどういう処理フローになっているかを読み解くことは非常に困難です。

解決策

そこで、エントリーポイントとなるStart()を一つのスクリプトのみで記述し、その他のスクリプトではInitialize()といったメソッドを用意して、Start()からInitialize()を呼びます。
これをすることにより、Start()は一つしか存在しないため、そこから処理をたどっていくことが可能となります。

※GameObjectのライフサイクルの問題などで、処理上どうしてもStart()が複数必要になるケースは棚上げします。

実装例

実際どのように実装するか一例をご紹介します。

開発環境

  • Windows 10 Version 20H2
  • Unity 2020.1.14f1
  • UniTask Ver.2.0.37

ヒエラルキー構成

Mainと命名したGameObjectをルートに作成し、その配下にGameObjectを役割ごとに階層構造に配置します。個人的には以下のような構成にするのが分かりやすくておすすめです。
ここで少し話が逸れますが、細かくプレファブ化をすることで、チームで一つのUnityプロジェクトをGitを使って開発する際に、同時に同じプレファブをいじらないようにすることで競合が起こりにくくすることができます。
image.png

Mainスクリプトの作成

Main.csを作成し、そこでエントリーポイントとなるStart()を記述します。Start()の中でその他のスクリプトのInitialize()を呼び、初期化処理を行います。また、それぞれの機能を実装したスクリプトのメソッドをMain.csから呼ぶことで、処理のフローの管理もしやすくなります。
これにより、Main.csを見れば大体の処理のフローを掴めるようにすることができました。
(今回の例では、カスタマイズ性が高いので、UniTaskでUpdate処理を置き換えています。)

Main.cs
using Cysharp.Threading.Tasks;
using System.Threading;
using UnityEngine;

public class Main : MonoBehaviour
{
    [SerializeField] private PlayerController m_PlayerController = null;
    [SerializeField] private AudioManager m_AudioManager = null;
    [SerializeField] private DataManager m_DataManager = null;
    [SerializeField] private InputManager m_InputManager = null;
    [SerializeField] private UIManager m_UIManager = null;
    [SerializeField] private XRManager m_XRManager = null;

    /// <summary>
    /// エントリーポイントです。
    /// </summary>
    private async UniTask Start()
    {
        await InitializeAsync();

        UpdateLoop(this.GetCancellationTokenOnDestroy()).Forget();
    }

    /// <summary>
    /// 初期化処理を実行します。
    /// </summary>
    private async UniTask InitializeAsync()
    {
        m_PlayerController.Initialize();
        m_AudioManager.Initialize();
        m_DataManager.Initialize();
        m_InputManager.Initialize();
        m_UIManager.Initialize();
        m_XRManager.Initialize();

        await UniTask.Yield();
    }

    /// <summary>
    /// Update処理を実行します。
    /// </summary>
    private async UniTaskVoid UpdateLoop(CancellationToken cancellationToken)
    {
        while (true)
        {
            // Updateで実行する処理をここに記述します。

            await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
        }
    }
}

まとめ

Mainスクリプトを読めば大体の処理のフローが分かるという安心感は、実装者側も読む側もハッピーにしてくれます。Unityは作るアプリケーションによって、適した構成も変わるので一概には言えませんが、処理のフローを分かりやすく作りたいと思っている方は参考にしてみてください。
すべてのUnity使いに幸あれ。

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

JUCEのGUIにUnityのViewを埋め込んでみる

本記事はJUCE Advent Calendar 2020 の12月1日向けに投稿した記事です。

JUCEって何?

JUCE (Jules' Utility Class Extensions)は、C++言語によるマルチメディア系アプリケーションの開発を支援するフレームワークです。クロスプラットフォーム設計のライブラリと、付属されているプロジェクトジェネレータ『Projucer』から各種IDE(VisualStudio, Xcode, Makefile)向けにプロジェクトファイルを出力することで、ワンソースからWindows, macOS, Linux, iOS, Android で動作するアプリケーションを作成することができます。
公式サイト

本記事の対象環境

OS: Windows 10
IDE: Visual Studio 2019
JUCE: JUCE v6.0.4

juce::HWNDComponentを使ってみよう

JUCE 6から追加されたjuce::HWNDComponentは、任意のHWNDを任意のコンポーネント内に埋め込むことができるWindows専用のクラスです。
このクラスは、GUIコンポーネント上に配置してから setHWND() を使用して任意のHWNDを割り当てるようにして使用します。

HWNDとは?

まずハンドルとは、ファイルや画像、ウィンドウなどを操作しようとする際に、その対象を識別するためにそれぞれに割り当てられる一意の番号のことを指します。
Windowsプログラミングにおいては、ウィンドウを識別するために各ウインドウに割り当てられる一意の番号をHWND型で取り扱うことになります。

juce::HWNDComponentの注意点

juce::HWNDComponentのAPIリファレンスには以下のように記述されています。

Of course, since the window is a native object, it'll obliterate any JUCE components that may overlap this component, but that's life.

意訳

もちろん、ウィンドウはネイティブオブジェクトなので、このコンポーネントと重なる可能性のあるJUCEコンポーネントはすべて消去されますが、仕方ないよね。

この課題については、@Talokayさんの記事が参考になります。

ここで起こる問題が、Heavy-Weight(ヘビー君)とLight-Weight(ライトちゃん) Componentの問題です。
VideoComponentに限らず、OpenGL、Web系のComponentは全てヘビー君であり、ライトちゃんであるJuceのComponentと同じレベルで使用すると、ヘビー君が常に、あらゆるライトちゃんよりも前面に表示される、という問題が起こります。

juce::HWNDComponentを含む複数のコンポーネントを配置する場合にはこの点に注意しておきましょう。

UnityをJUCEのGUIに埋め込む

ゲームエンジンのUnityには、Unityをネイティブアプリのライブラリとして実行することができる、Unity As A Libraryという仕組みが用意されています。

公式サイト (https://unity.com/ja/features/unity-as-a-library) より、

Unity では、ランタイムライブラリの読み込み、アクティベーション、アンロードの方法とタイミングをネイティブアプリケーション内で管理するための制御機能を用意しています。その上、モバイルアプリの構築プロセスはほぼ同じです。Unity では iOS Xcode と Android Gradle のプロジェクトを制作できます。

Unity As A Libraryの仕組みを利用することで、ネイティブアプリケーションの一機能としてUnityの実行を制御することができます。
Unity As A Libraryを利用することができるプラットフォームは、iOS/Android/Windowsに限定されます(本記事投稿時点)。
公式ドキュメント

作業手順

  1. Unity: 組み込みたいプロジェクトをビルドしておく
  2. JUCE: プロジェクトを作成する
  3. JUCE: Unity As A Library を呼び出す処理を実装する
  4. Unityのビルド成果物をネイティブアプリケーションから参照可能なパスにコピーする
  5. Unityのビルド成果物のうちDataフォルダXxx_Data(ネイティブアプリケーション名)_Dataにリネームする

JUCEからUnity As A Libraryを呼び出すコード

■ MainComponent.h

class MainComponent  : public juce::Component, public juce::AsyncUpdater
{
public:
    //==============================================================================
    MainComponent();
    ~MainComponent() override;

    //==============================================================================
    void paint (juce::Graphics&) override;
    void resized() override;
    virtual void handleAsyncUpdate() override;

private:
    //==============================================================================
    // Your private member variables go here...
    std::unique_ptr<juce::DynamicLibrary> unityLibrary;
    std::unique_ptr<juce::DocumentWindow> backendWindow;
    std::unique_ptr<juce::HWNDComponent> unityViewComponent;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

■ MainComponent.cpp

//==============================================================================
MainComponent::MainComponent()
{
    backendWindow = std::make_unique<juce::DocumentWindow>("UnityWindow", Colours::black, DocumentWindow::TitleBarButtons::allButtons, true);
    backendWindow->setSize(800, 600);

    unityViewComponent = std::make_unique<juce::HWNDComponent>();
    unityViewComponent->setHWND(backendWindow->getWindowHandle());
    addAndMakeVisible(unityViewComponent.get());

    setSize (800, 600);

    triggerAsyncUpdate();
}

MainComponent::~MainComponent()
{
}

//==============================================================================
void MainComponent::paint (juce::Graphics& g)
{
    g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
}

void MainComponent::resized()
{
    unityViewComponent->setBounds(20, 60, 600, 400);
}

void MainComponent::handleAsyncUpdate()
{
    juce::File dll = juce::File::getSpecialLocation(juce::File::SpecialLocationType::currentExecutableFile).getParentDirectory().getChildFile("UnityPlayer.dll");

    jassert(dll.existsAsFile());
    unityLibrary = std::make_unique<juce::DynamicLibrary>(dll.getFullPathName());
    auto um_func = (int (*)(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd))unityLibrary->getFunction("UnityMain");
    if (um_func)
    {
        auto* handle = unityViewComponent->getHWND();

        std::wstringstream stream;
        stream << L"0x" << handle;
        std::wstring hwnd = stream.str();

        std::wstringstream wss;
        wss << L"-parentHWND " << hwnd << L" delayed";
        std::wstring command = wss.str();

        auto command_utf16 = juce::CharPointer_UTF16(command.c_str());
        juce::String command_str(command_utf16);
        DBG(command_str);

        LPWSTR myWindowOutput = const_cast<LPWSTR>(command_str.toWideCharPointer());
        HINSTANCE hInstance = (HINSTANCE__*)juce::Process::getCurrentModuleInstanceHandle();

        um_func(hInstance, nullptr, myWindowOutput, 10);
    }
}

課題

手元で試してみたところ次の点で課題があったので諸々の工夫が必要そうです。

  • UnityのViewにキーボード入力のMessageを渡すことができない(マウスによる操作は可能)
  • UnityのViewにウインドウのフォーカスが占有されてしまい、親ウインドウのXボタンなどが利かなくなる

その他のフレームワークも埋め込んでみました

Sciter

Sciterとは、HTMLとCSSを使用して、クロスプラットフォームのデスクトップアプリを構築することを目的とした、アプリケーションフレームワークです。
http://sciter.com/

SciterのAPIには、ネイティブアプリケーションにEmbeddedすることを想定したAPIが用意されています。そのAPIを利用するとHWND型の値を取得できるので、Unityの場合と同じ要領でjuce::HWNDComopnentにHWNDを渡すことでSciterのViewを埋め込むことができました。

OpenSiv3D

OpenSiv3Dとは、C++ でゲームやメディアアートを作れるフレームワークです。クロスプラットフォームなライブラリ群とアプリケーションテンプレートがセットになっています。
https://siv3d.github.io/ja-jp/

OpenSiv3DのAPIには、ネイティブアプリケーションにEmbeddedすることを想定したAPIは用意されていません。それを可能にするために、ソースコードの一部を改変することで実現しました。具体的には次の手順を行っています。
1. OpenSiv3D製のexeファイルをJUCEアプリのサブプロセスとして実行する
2. サブプロセス起動の際にコマンドライン引数でHWNDの値をOpenSiv3Dに渡す
3. OpenSiv3Dのライブラリ中にあるウインドウ生成処理のコードに、引数から取得したHWNDを描画先として利用するように実装を変更する

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

VRMファイル(VRoid)にボーン付きのパーツを追加する

はじめに

以前、VRMに雰囲気を合わせてカスタマイズするという記事を書いた。その関連で今回はボーンが入ったアクセサリーなどのパーツをVRoidにフィットするように作る手順をまとめてみた。その他、作成上の注意点についてもその都度言及しておきたい。
 自分の覚え書きでもあるので、すでに詳しい方はぜひもっとこうしたほうがよいとかここは違うよ!などあればご指摘いただけると助かります。

基本の流れは私の次の記事に沿う。
VRMファイルをBlenderとUnityで雰囲気合わせてカスタマイズする

完成品見本

秋月 - アビス・ホライズン
・・・胸元のリボンと紐、腰のアクセサリーのフリル

アトランタ - 戦艦少女R
・・・しっぽ

ギアリング - 戦艦少女R
・・・ツインテール上部のカバーの結び目

参考サイト・書籍

VRoidモデルの髪を揺らしてみたい!(中編/blender作業編)

Vroid・Blenderで編集したモデルにUnityで物理演算を再設定する方法

Blender 2.8 3DCG スーパーテクニック

VRoidStudioですること

ベースとなるVRoidを作成する

 この段階で今回向けに注意することは特にない。一般的に3Dモデルを作る注意点はきっと多々あると思うので各サイトを参考にしよう。
 ただ、これから作るボーン入りのパーツをどこにあてはめるのか、イメージを掴んでおくとよい。髪型編集では比較的自由にパーツ目的で追加できる。仮置きでパーツを設置してみるとよりイメージが掴みやすい。

エクスポートする

 VRoidStudioでできることをすべてやったらエクスポートする。細かな設定は必要に応じてする。

Blenderですること

 UnityでVRM本体と合体するので必須ではないが、位置合わせのためにVRMを読み込みたい。事前にVRM_IMPORTERのアドオンをインストールしておこう。

目的のパーツを作る

 メッシュ(大体は立方体)を追加して移動・回転・拡大縮小、頂点・辺をひたすら増減させていき、目的のパーツを形作っていく。
 このあたりは一度作り方を覚えてしまえば、技術よりも個人のセンスが問われる部分だろう。

アーマーチュアを追加する

 ここからが今回のメインどころとなる。目的のパーツを対象の部位の近くに配置したら、
 オブジェクトモードにし、「追加」メニューから「アーマーチュア」→「単一ボーン」を選ぶ。

image.png

 すると八面体と上と下に球のついた図形がVRoidに重なって表示される。これがボーンだ。
image.png

 この図形はこのままVRoidに表示されるのではなく、あくまでもエディタ上での表示だ。実際にはこの図形を目的のパーツになぞるように重ねていくことになる。
 それが次の手順だ。

 ・・・とその前に、これから作業をするにあたりやっておくことがある。
image.png

 エディタ右のプロパティが並んだパネルのなかから図のように選び、「最前面」と「名前」にチェックを入れよう。
 こうすることで次の手順からのボーンの編集操作がわかりやすくなる。

ボーンを編集する

 アーマーチュアを選択した状態でTabキーを押して編集モードにする。あとはひたすら次の操作を移動を繰り返してボーンを目的のパーツに当てはめていく。

 なお、ボーンの選択位置によって移動の動きが変わる。

先端と末端の球体の移動:
image.png

 ボーンの先か末尾が動き、ボーンの方向や大きさが変化する。

ボーン自体(八面体のこと)の移動:
image.png

 先端と末端の球体も含めてまるごとボーンが移動する。

ボーンの追加:

 ボーンを追加するには「押し出し」を行う。
image.png

 このツールを選んでから先端か末端の球体を選択すると、図のようになる。
image.png

+マークを押したまま引っ張ると新たなボーンが出現する。
image.png

 なお、ボーンは八面体の 大 → 小 に流れるように設定していくことになる。(他の表示方法にしているとこれがわかりづらくなってしまう)
image.png

 この作業はVRoidStudioでいうところの「髪型編集」の「揺れもの」に相当する。
image.png

 Blenderでは自分で目的のパーツに沿ってボーンを押し出し・移動して設定していくのに対し、VRoidStudioでは目的のパーツ(髪型)にボーングループを設定すると、自動的に形に沿って設定してくれる。ボーンの数も揺れない箇所も揺れる強さもすべて簡単な設定でやってくれていた。
 両方を触るようになって、VRoidStudioがいかに3Dモデルを簡単に作れる神アプリなのかがよくわかった。

 ボーンを一通り追加し終わったら各ボーンの名前を変更しよう。この作業はのちのちUnityの段階で地味に効く。
 名前はデフォルトでは Bone → Bone.001 → bone.002 ... となっているはず。連番はそのままにして、「Bone」の部分を変えよう。

ボーンと目的のパーツ(のメッシュ)を関連付ける

 ここまでの状態では、ただ単にボーンを目的のパーツに合わせて作っただけである。動かそうとしても動かないので、これからの操作が必要となる。
 オブジェクトモードに戻した後、アーマチュアを選択し、それから目的のパーツ用に設置したすべてのメッシュを選択しよう。
image.png

 そして「オブジェクト」メニューから「ペアレント」→「アーマチュア変形」→「自動のウェイトで」を選ぶ。
image.png

 するとこのようにアーマチュアの中に必要なメッシュが移動する。子要素のようになっているが、意味合いとしてはグルーピングだ。アーマチュアに不要となれば後から分離させることもできる。

image.png

 ここまでやってボーンを動かしても、実はまだ目的のパーツ(のメッシュ)は動かない。ここからさらに、ボーンを動かしたときにどの頂点を動かすかを設定する必要がある。
 それが「頂点グループ」だ。

頂点グループを設定する

 メッシュを選択して「編集モード」に切り替える。右のパネルで気にするべきはこの設定である。
image.png

 ここの頂点グループの一覧に、さきほど設定したボーン(の名前)が並んでいる。
 並んでいない場合は自分で+を押し、ボーンと同じ名前で頂点グループを作成する。
 また、明らかに不要な頂点グループは「-」ボタンを押して削除しよう。

 この前の手順でボーンを作る際に、どのメッシュのどの部分に当てるかを位置関係である程度は把握はできているはず。ここではそれを実際に頂点で示して割り当てていく。

 対象の頂点グループを選択し、「選択」をクリックする。
image.png

 そうすると自動的に割り当てられている頂点がすべて選択される。これが想定通りであればよいが、大体意図しない選択になっているはず。
 意図しない頂点の選択になっていたら「選択」をクリックしてとりあえず頂点を選択したら、「削除」をクリックする。これでその頂点グループはまっさらな状態になる。
(念の為選択をクリックし、何も選択されないことを確認しよう)

 改めて頂点を選択していく。必要な頂点を選択し終えたら対象の頂点グループが選択されているか改めて確認し、「割り当て」をクリックする。
image.png

 メッシュの裏や隠れた箇所にも頂点がある可能性があるので忘れずに選択しよう。
 これで該当のボーンが動かされた時、どの頂点が動くかが紐付けられた。あとはこの作業をボーンの数だけ繰り返していく。
 

追随する強さ(度合い)を設定する

 今度はボーンの動きに追随する度合い(強さ)を設定する。まずは目的のメッシュのツリーを開き、設定したい頂点グループを選択する。
image.png

そして今度は「ウェイトペイント」モードにする。
image.png

 赤くなったり青くなっているのが見えるだろう。(見づらい場合はボーンの表示方法を変更しよう)

※ボーンの表示を「スティック」に切り替えた例
image.png

 そうするとこのようにボーンに対応したメッシュの部分が色分けされるようになる。これは実際にメッシュがその色になるわけではなく、ボーンが動いたときに頂点が追随して動く際の度合い(強さ)となる。

強い:赤 ← 黄色 ← 緑 ← 青:弱い

 というようになっている。該当部分を赤で塗りたくればその部分の頂点はしっかり追随して動くようになる。が、変形することにより妙なところが型くずれすることもある。
 青に限りなく近いと動きが弱くなる。激しく揺れてほしいところ・かすかに動くだけでよいところをあらかじめ考えておきたい。

 また、頂点・辺の位置関係・幅・高さ・角度によっても変形に影響がある。おそらく一発でうまく行くことはないはず。

きれいに塗る

 ウェイトペイントではペンやグラデーション・消しゴムツールを使って普通のお絵かきのように強さを決めていく。
 自分が体感したことだが、ペンでは塗りムラができてやりづらい。

 そこで積極的に使いたいのはグラデーションツールである。
image.png

 グラデーションツールは編集する際の線を引っ張っていき、その長さで塗りたい範囲の強さが決まる。事前に塗る強さを最大にしてからやれば赤をきれいに塗りやすくなる。
打って変わって塗る強さは0にしておけば、青をきれいに塗ることができる。

※エディタ上部の各スライダー
image.png

※グラデーションはこうして線を伸ばすとウェイトの値の通りに変化する。
image.png

 線を引っ張る角度や幅を工夫して繰り返していけば、きれいな塗りでウェイトを設定することができる。

ボーンをテストする

 一通りできたらいよいよボーンを動かすテストだ。単純な形であれば一発でうまくいくこともあるだろうが、大体はテスト→調整→テスト・・・とやり直すハメになる。
 右のツリーのアーマチュアの中の「ポーズ」を選択する。(大体の書籍ではショートカットキーで紹介しているが、画面のUIで紹介したほうが意識しやすいかもしれない)
image.png

 すると自動的に「ポーズモード」になる。

 そして「回転」などでボーンを動かしてみる。
image.png

想定通りに動くようになればOK!

image.png

ボーンのトランスフォーム(回転など)をもとに戻す

 ポーズモードで色々いじると元の形に戻せなくなることもある。そういうときには「ポーズ」メニューから「トランスフォームをクリア」→「キー状態にリセット」をクリックしよう。

image.png

 そうすると一発でもとに戻る。

ポーズの変形をデフォルトにする

 逆に、ポーズで変形した形を、そのアーマチュア(とメッシュ)のデフォルトの形にすることもできる。
 この場合は「適用」→「ビジュアルトランスフォームをポーズに適用」をクリックするとそのポーズがデフォルトのメッシュの形として反映される。

image.png

アーマチュアと各メッシュの位置に注意

 オブジェクトモードでのグローバル座標での位置と、編集モードでのローカル座標での位置を混在させたまま進めないように注意したい。
 自分はこの記事向けにサンプルを作ってる最中に、アーマチュアのボーンを編集モードで移動させたままで編集を続けてしまい、各メッシュとペアレント化した後にそれぞれを修正しまくってようやくこのとおりきれいに整えた。

image.png

 モデルの尻から伸びている点線が、グローバル座標での位置関係を示している。これがおかしいと、地面から各ボーンに点線が伸びていたりする。そうなったらアーマチュア・各メッシュのオブジェクトモードと編集モードを行き来して今の状態がデフォルトになるようにしよう。
 できればグローバル座標だけで目的のパーツ全体が動くようにしたい。(VRoidと位置合わせしやすいように)
 のちのちUnityでもこのグローバル座標とローカル座標でのトランスフォーム具合が反映されてしまうので、早め早めに修正をしておこう。

 各トランスフォームをし終わって、位置・回転・拡大縮小がこれでOK、となったら対象のアーマチュア・メッシュを選択し、「オブジェクト」メニューの「適用」→「全トランスフォーム」をクリックする。
 これでそのオブジェクトは現在の状態がデフォルトになる。

image.png

テクスチャを設定する

 これも必要だ。ただテクスチャはこの内容だけで膨大に長い記事にできてしまうので、詳しい説明は他所様にお譲りし、ここでは流れとポイントだけ書き残しておきたい。
 以前の私の記事も合わせて参照していただきたい。

シームマークをつける

 メッシュの辺に対して操作する。これをすると、メッシュオブジェクトは2Dのテクスチャ画像に展開されたとき、そこが切り取り線となり面が分割されるようになる。
image.png

 ペーパークラフトをイメージするとよいだろう。まさにあの通りだ。
 ペーパークラフトは2Dの紙から3Dの立体を作る。このシームマークはその逆をすることになる。

テクスチャ画像を作成する

 シームマークを付けたら、「UV Editing」に切り替える。
image.png

 まずはテクスチャ画像を本プロジェクト内に作成する。左の分割画面のこのボタンをクリックする。
image.png

 すると作成用のパネルがポップアップされるので、ここでテクスチャ名とサイズを指定する。
image.png

 そうすると本プロジェクト内でテクスチャ画像が編集できるようになる。

UV配置を作成する

 対象のメッシュを選択して「編集」モードにしたら、全ての辺を選択する。
image.png

 上部にある「UV」メニューより「展開」をする。
image.png

 そうするとさきほど付けたシームマークに沿ってメッシュが2Dに展開される。
image.png

 これが一発でうまくいくことはまずありえない。(四角形とか単純な形でない限り)
 だいたい・・・

・分割したはずの面同士が重なってる
・直角の辺と辺だったはずなのに斜めになってたり妙に長くなってる
・辺と辺が交差して面が裏返ってる

 こういうことになってるので、ここから地味に大変な調整作業になる。

 この展開の具合がおかしいと、テクスチャを適用した後の目的のパーツの3D表現もおかしくなる。ペーパークラフトでいうと、組み立て後の形がいびつになる。

 自分がやっていて注意しているのは次の点である。

・展開された面は全部選択してまとめて拡大縮小する(一部でも縮尺が違うとそのズレが全体に影響する)
・3Dモデルの大事な表現部分は可能な限り辺と面を整える
・単色塗りしかない部分はおおよそその形になるように整えるだけ(省力化)
・形が同じになるメッシュの部位はそこだけ切り取られるようシームマークを付け、展開図ではそれらを全部重ねる
・左右対称のメッシュはこの段階ではまだコピー等しない(似たパーツになるのなら同じ手順を繰り返さない)

一つの画像にUV配置を収める

 目的のパーツに使われるメッシュをすべて展開し終えたら、テクスチャ画像におさまるよう配置を整えよう。
 展開後の面の集まりをまるごと動かす場合は左上のこのボタンを押して切り替える。
image.png

 左から・・・点、辺、面、アイランド
 ・・・どうやら分割されたメッシュは「アイランド」と呼ぶのね・・・。
 まるごと動かしたい場合はこのアイランドにしておかないと、一部の頂点や辺を選択しのがして形が崩れてしまうこともありうるので注意したい。

 配置の仕方によっては重なったりはみ出たりするので、そういう場合は拡大縮小しよう。(注意点は上記)

UV配置をエクスポートする

 整え終わったらこの配置を画像にエクスポートしよう。左の分割画面の「UV」メニューから「UV配置をエクスポート」を選ぶ。
image.png

 あとはpng画像で保存し、好きなペイントソフトでレイヤー分けして塗りたくる。今回出力した画像は透明な背景に出力されるので、レイヤーの一番上に配置して塗るとわかりやすい。
image.png

この後は・・・

 テクスチャ画像を塗り終わったらBlenderに戻ってきて読み込ませよう。このあたりの手順と注意点は前回の記事を参考にしていただきたい。
 塗りと実際の3Dモデルへの反映を、納得できる形になるまで繰り返すことになる。

FBX形式でエクスポートする

 以前の記事ではobj形式で出力したが、今回はFBX形式で出力する。こうするとボーンなどの情報も合わせて保存される形式になる。

 なお、その前に各メッシュを「オブジェクト」モードでトランスフォームを全部適用しておく。
(これをしないとUnityエディタ上、実行上、VRoidHub上では問題なくても一部アプリ上で変形が妙に適用されてずれることがある)

エクスポートしたいオブジェクトを選択する

 ボーンのついたオブジェクトを選択する際に注意しておきたいことがある。
 通常はこのようにツリーの各オブジェクトの上部だけ選択することがほとんどだと思う。

 しかしボーンが付いている場合はこれではいけない。
 ツリー上のアーマチュアを開き、ShiftキーやCtrlキーを押しながらすべてのボーン・頂点グループを選択している必要がある。
image.png

 このように漏れなく選択しよう。
 最初自分はツリーの各メッシュ・アーマチュアの上位の部分だけ選択しFBXで出力していた。するどUnityではボーンがまったく存在しない状態になっていたのだ。普通であれば上部を選択したら配下のボーンやプロパティ的なものも含めて選択されていると考えてしまうが・・・。

image.png

 選択し終わったらエクスポートする。この時に「選択したオブジェクト」にチェックを入れておこう。
image.png

 これでエクスポートし終わった。

Unityですること

 ここからはUnityの作業となる。VRM編集用に空のプロジェクトを用意しておこう。すでにある場合は使いまわしても構わない。
uniVRMをダウンロードし、プロジェクトにインポートしておくことを忘れずに。

エクスポートしたファイルをインポートする

 Blenderの作業環境からUnityに持っていく必要があるファイルは次の通りとなっているはず。

  • [選択したメッシュオブジェクト・アーマチュアなど].fbx
  • [メッシュのテクスチャ画像].png

 この2つをUnityのAssetsフォルダにコピーする。(もしfbxやpngが複数ある場合はその分も)
image.png

マテリアルを作成する

 インポートが終わったら次はマテリアルを作成する。これをしないと、オブジェクトにはテクスチャが適用されず、あの灰色の無味乾燥なオブジェクトのままだ。
 マテリアルは、テクスチャ画像をUnityで正しく使えるようにする形式と覚えておくとよい。

 Assetsフォルダの適当な場所で右クリックし、「作成」→「マテリアル」を選んで作成する。
image.png

 それからその空のマテリアルを選択し、一番上のShaderのコンボボックスを開く。
 「VRM」→「MToon」と選択していく。
image.png

 次にテクスチャ画像をプロジェクトパネルから右のオブジェクトのインスペクタへとドラッグしていく。
・Texture → Lit Color, Alpha
・Texture → Shade Color
・Rim → Color
それぞれの名称の左側の四角い表示部分にドラッグすると、こうなる。
image.png

プレハブにマテリアルを適用する

 プレハブとは、先程インポートしたfbx形式のファイルのことを指す。これがUnityで扱える形式に変換されたのがプレハブだ。目的のパーツの全情報がプレハブ内に収まっている。ただし、テクスチャはどうもそうではないらしい。

 プレハブを選択すると右のインスペクタがこうなる。
image.png

 Materialsタブの「再マッピングされたマテリアル」でなし(マテリアル)になっているすべての欄に対し、先程作ったマテリアルファイルを設定していく。
image.png

 このようになればOK。最後に適用ボタンをクリックするとテクスチャが反映されるようになる。
image.png

パーツをVRoidの目的のボーンに配置する

 今回の目的のボーン入りのパーツをVRoidの体の箇所にドラッグして設定する。位置関係がずれていれば調整しよう。正しく配置したらこうなる。
image.png

VRM Spring Boneを設定する

 せっかくBlenderでボーンを設定しても、そのままだとどうやらUnity ではボーンを正しく認識してくれていないらしい。そのため、改めてボーンを認識させてやる必要がある。
 それが「VRM Spring Bone」だ。
 インスペクタから一番下の「コンポーネントを追加」をクリックし、「スクリプト」→「VRM」→「VRM Spring Bone」とたどって選択しよう。
image.png

 するとこのようなプロパティが表示される。
image.png

ボーンを再設定する

 Root Bonesの部分を開き、サイズをBlenderで作成したボーンの数指定する。するとこうなる。
image.png

 各要素の欄をクリックするとインポートしたパーツが保持するボーンを選べるので、正しいボーンを選んでいく。
image.png

 ここではBlenderで作成したボーンを最上部から順に選んでいくということ。中には多段階層になっているボーンもありうるだろうが、それらもBlenderのボーンの表示順の通りにここでは選んで再設定していく。
 そして●●●_end という表記のボーンは選んではいけないらしい。(詳細は本記事で参照したリンク先のサイトを参照)

 設定し終わるとこうなる。
image.png

ボーンの稼働範囲を制限する

 ここまででもうVRoidで追加パーツのボーンが動くようになっているが、もうひと手間加えておきたい。それが「VRM Spring Bone Collider Group」というコンポーネントだ。(これも参照先のサイト詳しい説明がある)
 これが非常に便利。ボーンが動く範囲を細かく調整できるのだ。

インスペクタから一番下の「コンポーネントを追加」をクリックし、「スクリプト」→「VRM」→「VRM Spring Bone Collider Group」とたどって選択しよう。
 するとこのようなプロパティが表示される。
image.png

 次に「コライダー」→「サイズ」を好きな値に指定する。これを指定するとこうなる。
image.png

 シーン画面のVRoidのそばに色のついた球体が表示される。サイズや位置はインスペクタの矢印の部分で細かく指定できる。これを今回目的のボーン入りのパーツ(しっぽ)に近づける。

 ここから先はゲームを実行しながらの方がわかりやすいので、必要に応じて切り替えよう。(設定は後で反映しよう)

 コライダーを作成し、大きさを決める。そしてVRoid自体の位置座標を変更して動かしてみて、ボーンの可動範囲を確認する。
image.png

 VRoidを激しく動かして見て、今回目的のパーツが妙な方向に吹っ飛んだらその位置にコライダーを設置し、稼働を制限する。また別の箇所・・・といったように確認と設定を繰り返していこう。

 ゲーム実行中にこれでよいと思ったら、すぐにゲームを終えてはいけない。ゲーム実行中のコンポーネントの値変更は破棄されてしまう。
 設定値を保持したい場合、コンポーネントの右上の「・・・」をクリックし、そこから「コンポーネントをコピー」をクリックしよう。
image.png

 ゲームを停止して通常の編集状態に戻ったら、すぐに対象のコンポーネントの右上をクリックし、「コンポーネントの値を貼り付け」をしよう。
image.png

 これで納得行く設定が反映される。

uniVRMでエクスポートする

 他に付ける必要があるパーツがあるならそれも同様に設定しよう。ボーンありなしではこれまでに行ってきた手順がだいぶ増減する。
 一通り設定して問題なければ、いよいよVRMファイルとしてすべて結合してエクスポートすることになる。
 エディタ上の「VRM」メニュー→「UniVRM-0.xx.x」→「Export humanoid」を選ぶ。
image.png

 そして必要な情報を入力したら「Export」ボタンをクリックする。
image.png

VRoid対応アプリで動作確認する

 PC上から読み込み可能なアプリで動作確認し、ボーンの揺れ方が問題なければ完成だ。

終わりに

 おおよそ前回記事の作業流れの補完の意味合いも兼ねての今回の記事となってしまった。各ツールの使い方に慣れは必要だろうが、この流れでVRoidにお手製の好きなパーツを合体して独自のVRoidを作れるようになるはずである。
 VRoid、Blender、Unityいずれも今年始めて使う自分でもここまでできるようになるのだから、もっと自由に独自のVRoidを作って公開する人が増え、VRoidがフォーマットとして普及することを期待したい。

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

【Unity】Unity上からiOS端末の発熱状態を取得する

iOS端末の発熱状態を取得できるAPIが無いか調べていたところ、ProcessInfo.thermalState
から取得できることが分かりました。(※ドキュメントを見るに対応OSはiOS 11.0+である必要がありそう)

この値を知ることが出来れば、例えば重いシーンなどで状態を見てサーマルスロットリング対策を可変的に機能させる処理を書くことが出来るかもしれません。(e.g. 熱状態がヒドいときに一部の機能のクオリティや更新頻度を下げるなど)

今回はこちらをUnityからも参照できるようにネイティブプラグインも合わせて実装したのでメモ。
※コードだけ先に見たい方はこちら参照

  • Unity 2019.4.15f
  • Xcode 12.2

ProcessInfo.ThermalState について

thermalStateを参照するとProcessInfo.ThermalStateと言うenum型が返ってきます。
こちらは以下の様に定義されており、現在の端末の発熱状態を知ることが出来ます。

    // Describes the current thermal state of the system.
    @available(iOS 11.0, *)
    public enum ThermalState : Int {

        // No corrective action is needed.
        case nominal = 0

        // The system has reached a state where fans may become audible (on systems which have fans). Recommendation: Defer non-user-visible activity.
        case fair = 1

        // Fans are running at maximum speed (on systems which have fans), system performance may be impacted. Recommendation: reduce application's usage of CPU, GPU and I/O, if possible. Switch to lower quality visual effects, reduce frame rates.
        case serious = 2

        // System performance is significantly impacted and the system needs to cool down. Recommendation: reduce application's usage of CPU, GPU, and I/O to the minimum level needed to respond to user actions. Consider stopping use of camera and other peripherals if your application is using them.
        case critical = 3
    }

コメントには状態及び推奨される解決策などが記載されており、ざっと和訳すると以下のように分類できそうです。

  • nominal
    • 正常動作の範囲内
  • fair
    • 熱状態がやや上昇している
    • 推奨される対策:
      • ユーザーに表示されないアクティビティの遅延
  • serious
    • 熱状態が高い
      • (ファン搭載機の場合には)最大速度で動作しているために、システムのパフォーマンスに影響を及ぼす可能性がある
    • 推奨される対策:
      • 可能であればアプリのCPU,GPU,I/Oの使用率を下げる
      • 視覚効果を低品質のものに切り替えるなどしてフレームレートを下げる
  • critical
    • 熱状態がシステムパフォーマンスに大幅な影響を与えるレベルまで達したので、デバイスを冷やす必要がある
    • 推奨される対策:
      • アプリのCPU,GPU,I/Oの使用率をユーザーアクションに応答するために必要な最小限レベルまで削減すること
      • アプリがカメラやその他周辺機器を使用している場合には使用を停止することを検討

Unity上で参照するには

thermalStateはProcessInfoが持つstatic変数から参照することが出来るので、そのまま流すNativePluginを書くことでUnity上からも参照することが出来ます。(要iOSビルド)

以下にコードを載せておきます。
(※P/Invokeに於ける便宜上、ObjectiveC++で実装してます)

ProcessInfoWrapper.mm
#import <Foundation/Foundation.h>

@interface ProcessInfoWrapper : NSObject

// 結果はNSProcessInfoThermalStateに準拠
// - https://developer.apple.com/documentation/foundation/nsprocessinfothermalstate?language=objc
+ (int)getThermalState;
@end

@implementation ProcessInfoWrapper
+ (int)getThermalState {
    return (int) [NSProcessInfo.processInfo thermalState];
}
@end


#ifdef __cplusplus
extern "C" {
#endif

int __getThermalState() {
    return [ProcessInfoWrapper getThermalState];
}

#ifdef __cplusplus
}
#endif
ProcessInfoWrapperBridge.cs
using System.Runtime.InteropServices;

namespace iOSNative
{
    public static class ProcessInfoWrapperBridge
    {
        [DllImport("__Internal")]
        static extern int __getThermalState();

        /// <summary>
        /// 現在のiOS端末の発熱状態の取得
        /// </summary>
        /// <returns>iOS実機以外は常に.Nominalを返す</returns>
        public static ThermalState GetThermalState()
        {
#if !UNITY_EDITOR && UNITY_IOS
            return (ThermalState) __getThermalState();
#endif
            return ThermalState.Nominal;
        }
    }
}
ThermalState.cs
namespace iOSNative
{
    // https://developer.apple.com/documentation/foundation/processinfo/thermalstate
    public enum ThermalState
    {
        /// <summary>
        /// The thermal state is within normal limits.
        /// </summary>
        Nominal,

        /// <summary>
        /// The thermal state is slightly elevated.
        /// </summary>
        Fair,

        /// <summary>
        /// The thermal state is high.
        /// </summary>
        Serious,

        /// <summary>
        /// The thermal state is significantly impacting the performance of the system and the device needs to cool down.
        /// </summary>
        Critical,
    }
}

スクリプトからはProcessInfoWrapperBridge.GetThermalState()を参照することで状態を取得できます。

おまけ: 動作確認

簡単な動作確認としてGPUに負荷の掛かりそうな以下のシーンを用意して、手持ちのiPhoneXで動作確認を行ってみました。

シーン自体は大量の半透明なPlaneを重ねて配置しているだけです。(2枚目はOverdraw)

Shared.png

Overdraw.png

これをコルーチン上から1秒間隔で熱状態のポーリングを行い、結果を色とテキストで表示するようにします。

IEnumerator Check()
{
    // 発熱状態を1秒間隔でポーリング
    while (true)
    {
        var thermalState = ProcessInfoWrapperBridge.GetThermalState();

        // 結果の表示
        _text.text = thermalState.ToString();
        switch (thermalState)
        {
            case ThermalState.Nominal:
                _text.color = Color.green;
                break;
            case ThermalState.Fair:
                _text.color = Color.yellow;
                break;
            case ThermalState.Serious:
                _text.color = new Color32(255, 165, 0, 255);
                break;
            case ThermalState.Critical:
                _text.color = Color.red;
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }

        yield return new WaitForSeconds(1);
    }
}

結果

起動したばかりの何も表示されていない状態ではNominalとなってます。

iphonex_resize.jpg

こちらを先程の大量のPlaneを表示して適当に放置すると、以下のようにSeriousに変わっていることが確認できました。

IMG_3454_resize.png

リンク

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

RayFire for Unity を使う

Unity を使って、3Dモデルをバラバラに粉砕する演出ができるアセットを見つけたのですが、性能面でちょっと嵌ったので、顛末をまとめました。

1. とりあえず分解してみる

分解したいオブジェクトにRayFire Rigid コンポーネントをアタッチします。
対象となるオブジェクトにはMesh Filter とMesh Renderer がアタッチされている必要があります。
(この例では、初期状態で対象となるオブジェクトにRididBody はアタッチしていないので、自由落下はしません。)

image.png

エディタ上でRayFire Rigid コンポーネントを見るとDemolish ボタンがあるので、再生モードにしてからこのボタンを押してみます。
すると、ヒエラルキーツリーにRayFireMan オブジェクトが生成され、その下に元のメッシュが粉砕された新たなメッシュが生成されます。

image.png

RigidBody が粉砕パーツに付与され、自由落下していきます。他の物体にぶつかると、粉砕パーツがバラバラになっていきます。

image.png

なお、エディタ上での操作ではなく、スクリプトによって粉砕処理を実施する場合は RayFire.RayfireRigid コンポーネントのDemolish() メソッドを呼び出せばOKです。

2. メッシュ分割を前処理しておく

ドキュメントには「RayFire Rigid component」の項に以下のように書いてありました。

• Manual Prefab Precache: Special type if you want to manually precache mesh data in prefab and save it as asset in project folder. Regular Precache type can not store mesh data as assets because Unity Mesh is not serializable. Precached Prefab stores mesh data in serializable format way which allows to store it as asset.

Manual Prefab Precache を使えということですね。ドキュメントには参考用の動画も貼られていました。

https://www.youtube.com/watch?v=_aO08dcqBOQ&feature=youtu.be

ここの通りにやれば解決できるということでしょう。

3. アセットの実体とドキュメントが整合していない?

実際のアセットを動かしてみると、肝心の「Manual Prefab Precache」が見当たらりません。なぜだ?
もしかして、バージョンアップに伴って廃止された?

早速ChangeLog を見てみると以下の記載を発見。

[1.23]
- Rigid. Removed Obsolete Manual Precache, Manual Prefab Precache and Manual Prefragment demolition types.
Replaced by Reference Demolition: Prefragment with Shatter -> Mesh Export -> Prefab -> Reference Demolition.

2020/11/28 時点での最新版は1.24 なので、前のバージョンで手動系のいくつかの機能が抹消されているようですね。。。
一応、代替の方法が書かれてあったので、これを試してみました。要は、

  • Prefragment でバラバラになったメッシュ群をMesh Export でPrefab にして、
  • RayFire のRayFire Rigid component のDemolish Type をReference Demolition にして、
  • バラバラになったPrefab を参照させる

ということでしょう。バラバラになった瞬間に、バラバラPrefab を生成して瞬時に置き換えるのですね。
アセットの構造をシンプルにしたかったがための対策なのでしょうかね。

早速試してみました。

4. 事前準備

まず、Mesh Export なるものを実行するための準備をします。

https://techblog.kayac.com/export-unity-mesh-as-obj

上記のサイトを参考にして、Export するための機能をエディタに追加します。
これはエディタでの作業の際にだけ必要ですので、以下のようなクラスを追加しました。

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class MeshExporter : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }

#if UNITY_EDITOR
    [MenuItem("CONTEXT/MeshFilter/Save .asset")]
    public static void SaveFromInspector(MenuCommand menuCommand)
    {
        var meshFilter = menuCommand.context as MeshFilter;
        if (meshFilter != null)
        {
            var mesh = meshFilter.sharedMesh;
            if (mesh != null)
            {
                // 保存するパスを指定する
                var path = string.Format($"Assets/{mesh.name}.asset");
                AssetDatabase.CreateAsset(mesh, path);
                AssetDatabase.SaveAssets(); 
            }
        }
    }
#endif
}

5. バラバラPrefab の生成

バラバラに粉砕したい対象にRayFire Rigid コンポーネントをアタッチし、Main -> Demolition Type をAwake Prefragment にします。

image.png

そして、エディタを再生開始し、一時停止状態にします(分解した状態を留めるため)。
RayFire Rigid コンポーネントのDemolish ボタンを押し、前述したRayFireMan オブジェクト以下に生成される新たなメッシュを確認します。

image.png

このままでPrefab 化してもメッシュ部分が消えてしまい、Reference Demolition で参照できる状態にならないので、メッシュ部分をExport します。

  1. 分解されたパーツを選択する
  2. MeshFilter コンポーネントを選択し、Save .asset を選択する
  3. Asset フォルダの直下に分解パーツのメッシュが出力されているのを確認する(適宜適当な場所に移動する)
  4. 分解パーツの親玉もPrefab 化する(3. によりメッシュを消滅させずにPrefab 化できる)

以下の図も参考にしてください。

image.png

これで分解後のPrefab オブジェクトが完成しますので、エディタの再生を終了します。

6. Reference Demolition の設定

  1. 先ほどのRayfire Rigid コンポーネント(Demolition Type をAwake Prefragment にしていたもの)を更に変更し、Demolition Type をReference Demolition に変更する
  2. Reference Source に分解されたパーツの親玉Prefab を設定する

image.png

これで、RayFire.RayfireRigid コンポーネントのDemolish() メソッドを呼び出すと、粉砕されたPrefab により新たな粉砕済みインスタンスが生成されます。

7. その他

粉砕する時のメッシュ分割処理も重たいのですが、それ以上に物理演算処理もかなり重たいです。大きな理由は、粉砕パーツはすべてMesh Collider によって当たり判定処理が行われるためです。そのため、発生頻度の低い限定的な演出に使用するか、発生頻度が高い場合は物理演算処理をサボるような工夫をするとか、まだまだ考えることがありそうです。

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

【Unity ARFoundation】平面検知した場所にドアを置く&タップしてドアを開閉する

はじめに

UnityのAR Foundationを使って「平面検出した場所にタップしてドアを置く&そのドアをタップすると開閉する」というものを作ってみました。

やろうとしたこと

平面を検出する

検出した平面にドアを配置

ドアをタップするとドアが開く、閉じる

結果

完成したものはこちらです。

補足

・デフォルトの平面検出のPlane(AR Default Plane)が好きではなかったので、Shaderを使用して変更しています。
こちらのサイトを参考にしました。

【Unity】AR Foundationの平面認識のPlaneをカスタマイズする

・ドアを設置したあとも平面検出のPlaneが残ったままなので、ドアを設置したら平面検出のPlaneを消すようにしました。

・タップした場所にパーティクルエフェクトを入れました。

感想

もともとARに関してはSwiftのARKitを勉強していて、Unityは最近はじめたばかりで理解が浅く、かなり時間がかかりました。
特にタップしてドアを設置した後にドアをRaycastで開閉するところでかなり苦戦しました。

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

ARとAIを組み合わせて遊ぶ

ARとAIを組み合わせて遊ぶ

はじめに

拡張現実(AR)と人工知能(AI)を組み合わせて遊ぶとお互いの足りない点を補っていろいろと面白いことができると思います。
いずれも使える技術は揃ってきており、組み合わせて使うための開発環境や実行環境もだいぶ整ってきていると思います。主にスマホでARとAIを組み合わせたアプリを作るのであれば、開発環境、ライブラリ、実行環境は以下のツールがサポートされています。

AR開発ではUIの設計や3Dモデルのあ使いやすさから、Unityで開発することが多いと思います。AR FoundationはAndroid、iOSでの動作をサポートしている(機能差はある)ため、Unityで両端末向けに開発すれば良いでしょう。AIについてはこちらのレポジトリがUnityでTensorflowLiteを動かすライブラリを公開してくれており、これもAndroid、iOS両方で動かすことができます(ありがたや、ありがたや)。
ARアプリでAIを使う利点のひとつは、高度な画像認識や画像変換が可能なことです。AIはパターンマッチングで認識するよりも種類多く、高い精度で物体を識別することが可能です。加えて画風変換や生成等も可能になっています。今回はAR FoundationとTensorflow Liteを組み合わせて、簡単なデモアプリを作ってみます。

作るもの

ネコを認識して、ネコの周りに3Dの立方体を出現させます。
こういうイメージです。

cat detection

やっていることは

  • 物体検知でカメラ内にネコを見つける
  • → 検知したネコの周りに立方体を出現させる

という簡単なものです。

コードはこちらで公開しています。
https://github.com/shibuiwilliam/AR-AI

作り方

環境

  • Unity 2020.1.14f1
  • OS: Macbook Pro

ライブラリ

まずは必要なライブラリをPackage Managerからインストールします。
主に以下のライブラリとバージョンになります。

  • AR Foundation 3.1.6
  • ARCore XR Plugin 3.1.8
  • ARKit XR Plugin 3.1.8
  • Mathematics 1.2.1

プロジェクトに入れているライブラリ一式は以下になります。

packages.png

Tensorflow Liteのみ、こちらのレポジトリから拝借しています。こちらのレポジトリはUnityでTensorflow Liteを動かすならとても便利なライブラリを用意してくれており、大変助かりました。ありがとうございます!

Scene

Sceneを作っていきます。まずはHierarchyにあるCameraを削除し、追加でAR Session Origin、AR Session、AR Cameraを作ります。
AR Session Origin配下にAdd ComponentからAR Plane ManagerとAR Raycast Managerを追加します。
加えてScriptとして AR Placement Manager を作成します。

scene.png

モデル

Tensorflow Liteのモデルはtfhubtfliteサンプルモデルから学習済みのものを入手することができます。
今回は物体検知としてSSDを使います。
SSDは軽量な物体検知モデルで、カメラ画像に対して検知した物体の位置(x,y,width,height)、検知した物体、検知スコアを返します。
検知可能な物体はここにあります。

Script

作成したARPlacementManager.csにコードを書いていきます。
void Start()でモデルをロードし、cameraTextureを初期化します。

using System;
using System.IO;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
using UnityEngine.UI;
using TensorFlowLite;

[RequireComponent(typeof(ARRaycastManager))]
public class ARPlacementManager : MonoBehaviour
{
    [SerializeField] 
    string detectionFile = "ssd_mobilenet_v1_1_metadata_1.tflite";

    [SerializeField]
    private GameObject arCamera;

    [SerializeField]
    private ARCameraBackground arCameraBackground;

    [SerializeField]
    int interval = 1;

    private ARRaycastManager raycastManager;
    private static List<ARRaycastHit> hits = new List<ARRaycastHit>();

    RenderTexture cameraTexture;
    RawImage cameraView = null;

    SSD detector;

    TextureToTensor textureToTensor;

    DateTime last;

    private void Awake()
    {
        raycastManager = GetComponent<ARRaycastManager>();
    }

    void Start()
    {
        var options = new InterpreterOptions()
        {
            threads = 4,
            useNNAPI = false,
        };

        string path = Path.Combine(Application.streamingAssetsPath, detectionFile);
        detector = new SSD(path);

        textureToTensor = new TextureToTensor();

        cameraTexture = new RenderTexture(Screen.width, Screen.height, 0);
        cameraView.texture = cameraTexture;

        last = DateTime.Now;
    }

物体検知はvoid Update()で1回/秒で実行しています。インターバルを置かなくても良いのですが、画面がカクカクするのを防ぐために回数制限しています。
検知対象のカメラ画像はcameraTextureで取得しています。
今回はネコを検知したいので、ネコラベルの16番を検知したときに検知位置に立方体を出現させています。

    void Update()
    {
        if (arCameraBackground.material != null)
        {
            DateTime now = DateTime.Now;
            if ((now-last).TotalSeconds >= interval)
            {
                Detect();
                last = now;
            }
        }
    }


    private void Detect()
    {
        Graphics.Blit(null, cameraTexture, arCameraBackground.material);

        detector.Invoke(cameraTexture);
        var results = detector.GetResults();

        var catPos = -1;
        var catProb = 0f;
        for (int i = 0; i < results.Length; i++)
        {
            if (results[i].classID == 16)
            {
                if (results[i].score > catProb)
                {
                    catPos = i;
                    catProb = results[i].score;
                }
            }
        }

        if (catPos != -1)
        {
            var x = (results[catPos].rect.x + (results[catPos].rect.width / 2)) * Screen.width;
            var y = (results[catPos].rect.y + (results[catPos].rect.height / 2)) * Screen.height;
            Debug.Log($"X Y: {x}, {y}");
            var pos = GetPosition(x, y);
            Debug.Log($"get position: {pos.x}, {pos.y}, {pos.z}");

            GameObject tmp = GameObject.CreatePrimitive(PrimitiveType.Cube);
            tmp.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f);
            Debug.Log("Placed object");
            Instantiate(tmp, pos, Quaternion.identity);
        }
    }

    private Vector3 GetPosition(float x, float y)
    {
        var hits = new List<ARRaycastHit>();
        raycastManager.Raycast(new Vector2(x, y), hits, TrackableType.All);

        if (hits.Count > 0)
        {
            var pose = hits[0].pose;
            return pose.position;
        }
        return new Vector3();
    }

}

物体検知で取得するネコの位置はカメラ内の(x, y, width, height)であり、奥行きは取っていません。奥行き含めたAR Session内の位置(x, y, z)は RaycastraycastManager.Raycast(
Vector2 screenPoint, List<ARRaycastHit> hitResults, TrackableType trackableTypes = TrackableType.All)
で変換することができます。
AIに限らず、多くの画像処理が2次元をベースに開発されているため、ARの3次元ベクトルに変換しようとすると、2次元のY軸から3次元のY軸とZ軸を取得することになります。このあたりはメソッドがpublicだと助かりますが、公開されていないと困り処の一つになります。

おわり

というわけでARにAIを組み合わせた簡単なアプリを作りました。本当はもっと凝ったものを作りたかったのですが、時間切れです。年末年始にいろいろ作ります。

 補足

ちょうど一年くらい前にARカメラでセマンティックセグメンテーションを実行して、写っているネコの分身を作るアプリを作っていました。しかしARCoreがv1.20.0に更新されたときに利用していたAPIが消えてしまい、すべてが失われてしまいました。
もったいないので、こういうこともできるということで動画とmedium記事を残しておきます。

cat segmentation

https://medium.com/@shibuiyusuke/playing-android-with-firebase-ml-kit-tensorflow-lite-and-arcore-4-b4ac12d34269

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