AYU MAX

プログラミングとか作ったもの

UE4でARKitの物体検出機能を使う

ARKitの物体検出(Object Detection)

物体検出機能はARKit2から利用可能になった機能で、あらかじめスキャンした物体のデータを登録しておくと作成したARアプリケーションから登録済みの物体を検出することができるようになります。

この機能についてはUnityを使った事例はいくつか記事になっているのですが、UE4を使った事例は見つかりませんでした。

今回どうしてもこの機能をUE4(4.22)で使いたくて、色々試し動かすことに成功したのでやり方をメモしておきます。

オブジェクトのスキャン

まずは検出する物体のスキャンをします。

UE4を使ったスキャン

UE4を使ってスキャンする場合は、まずARSessionConfigのSession Typeを「Object Scanning」にしてから、「Get AR Candidate Object」を使用して行うらしいです。

f:id:ayuma0913:20190815221119p:plain

f:id:ayuma0913:20190815221316p:plain

上で「らしい」という言葉を使ったのは理由があり、現状上記やり方ではスキャンは成功してないです。

「Get AR Candidate Object」をコールした際にアプリケーションが落ちてしまいスキャンはできませんでした。

成功した方がいらっしゃったら、教えてください。

Apple提供ツールを使ったスキャン

以下のサイトの上部にある「Download」ボタンを押すと、オブジェクトスキャンをするためのiOSアプリのプロジェクト一式がダウンロードできます。

developer.apple.com

ダウンロードしたプロジェクトをXCodeを使って、自分のiOS端末にいれて使用します。 使い方は結構直観的に分かり、最初に黄色いBoxで領域を確定した後、各面毎にスキャンを行います。

黄色いBoxの中に小さい黄色い点(多分物体の特徴点)がたくさんあったほうが後の検出率が高いような気がしています。

f:id:ayuma0913:20190815221654p:plain

スキャンが終わると*.arobjectという拡張子のファイルが生成されるので、iCloudとかを利用して開発マシンまでファイルを持っていきます。

XCodeやUnityを利用したARアプリ開発の場合は上記方法で完結しており、*arobjectのファイルがそのまま利用できます。

UE4の場合はそのままは利用できず、ダウンロードしたスキャンアプリを一部書き換える必要があります。

ViewController.swiftのcreateAndShareReferenceObjectメソッド

変更前

try object.export(to: documentURL, previewImage: testRun.previewImage)

変更後

try NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: false).write(to: documentURL)

上記書き換え部分はファイルを生成する部分なのですが、利用する関数が違います。

変更前はARReferenceObjectクラスのexportを使っているのですが、これをNSKeyedArchiverを使ってバイナリ化した後そのままファイルに書き込むように変えました。

またオリジナルと区別するためファイルの拡張子を(*.arobject2)に今回はしています。

これはUE4のARKitプラグインでバイナリを復元する際(FAppleARKitConversion::ToARReferenceObjectSet)に使われているのがNSKeyedUnarchiverだからです。

最初書き換えなしで*.arobjectのファイルを読み込ませたところ、ARReferenceObjectの復元に失敗したため調査したところ上記書き換えで成功するようになりました。

arobjectファイルにはUnityで読み込むと静止画のサムネイルが表示されていたので、そういった付加情報が*.arobjectには入っており後者のファイルとはバイナリに保存される内容が違うのが原因かなと思っています。

スキャンデータのUE4への取り込み

ARCandidateObjectアセットの作成

スキャンして生成された*.arobject2ファイルをUE4に取り込みたいのですが、今回は専用のアセット生成クラス(UFactoryを継承したクラス)をC++で書きました。

ファイルの中身は以下になります。

ここで気を付ける点としては、上記C++クラスはEditorの機能を利用するため、UnrealEdモジュールへの参照追加の必要があります。

また自分のプロジェクトのRuntimeモジュールにいれてしまうとアプリの書き出し時にエラーがでるので、Editorモジュールにいれてください。

このモジュールについては以下のヒストリアさんのサイトが分かりやすいです。

[UE4] モジュールについて|株式会社ヒストリア

上記ファイルをプロジェクトに追加してビルドすると*.arobject2ファイルをドラッグすることでファイルがインポートされてARCandidateObjectのデータアセットが作られます。

f:id:ayuma0913:20190815224018p:plain

アセットを開くと「Candidate Object Data」の配列に大きな数字が入っているのが分かると思います。

ここに*.arobject2ファイルの中身がバイナリ形式で入っています。(配列を開くと数が多いのでとても重いです。開かないほうがいいと思います)

あと、この画面で「Friendly Name」に名前をつけておきます。この名前は検出時に利用できます。

画面下部の「Bounding Boxについては、ARKitプラグインのソースを見ても利用されていなかったので現状入力しなくて良いと思います。

f:id:ayuma0913:20190815223954p:plain

ARSessionConfigへの登録

作成したARCandidateObjectアセットはARSessionConfigに登録します。

この辺りはイメージ検出の時と同じです。

ARSessionConfigアセットの「Candidate Objects」に作成したアセットをいれます。

f:id:ayuma0913:20190815224604p:plain

オブジェクトの検出

いよいよオブジェクトの検出になります。

「Get All AR Geometries」を呼ぶと検出されているすべての認識物(平面とかイメージとかとか)がとれるので、そこから「ARTrackedObject」にキャストできたものを選択しています。

「Get Friendly Name」で事前に登録しておいたNameが取得できるので、これを使ってその後の処理を振り分けることが可能です。

f:id:ayuma0913:20190815225008p:plain

実行イメージ

これで実行するとこんな感じになります。

プリキュアのキャラの人形を認識して青いBoxを生成しています。

まとめ

今回どうしてもUE4を使って物体検出がしたかったので、試行錯誤してどうにか成功しました。

結果的に動いたので良いのですが、もうちょっと簡単にできたらいいのになあと思います。 理想的にはUnityと同じように*.arobjectをそのまま取り込めると誰でも使えて良いですよね。

ARKitプラグインを書き換えればそのようにもできそうですが、今後のエンジンアップの度にやるのも嫌なので当分は今回のやり方でいこうと思います。

もしもっと簡単にできるやり方があればどなたか教えて下さい!

ObjectDeliverer Ver 1.3.0リリース マルチプラットフォーム対応

ObjectDeliverer Ver 1.3.0

f:id:ayuma0913:20190731231924p:plain

本日ObjectDelivererの新バージョンをリリースしました。

www.unrealengine.com

内容は以下の2点です。

  • iOS, Android, Macへの対応
  • UDPReceiverを使用した時にエディタ終了時まれにフリーズする不具合を修正

iOS, Android, Macへの対応

最近個人的にUE4でモバイルアプリの作成を開始したこともあり検証環境ができてきたので、モバイルOSの対応を実施しました。

ただ内部的なコードの変更は何もしておらず、各種OS向けにパッケージを作成したのみとなります。

もともとほとんどの仕組みをUE4のC++クラスのみを利用して作成していたため、今確認できる範囲では各種OSでもちゃんと動作が確認できています。

これによりスマホアプリとデスクトップアプリや、スマホアプリ同士での通信ができるようになるため、プラグインとしての使い勝手が今まで以上に広がると思っています。

UDPReceiverを使用した時にエディタ終了時まれにフリーズする不具合を修正

もう1点は不具合修正になります。

UDPの受信機能を使用している場合に、タイミングが悪いとエディタ終了時にフリーズしてしまう不具合があったため修正しました。

他のプラグインの対応

現状ObjectDeliverer以外のプラグインは全てWindowsのみの対応としていますが、今回の対応で対応方法が分かってきたので、他のプラグインも時期をみてモバイルOSの対応も実施したいと思っています。

UE4でウィンドウキャプチャーできるプラグイン(WindowCapture2D)をマーケットプレイスに公開しました

WindowCapture2Dとは

f:id:ayuma0913:20190707230148p:plain

Unreal Engineで使えるWindow Captureのプラグインです。

Windowsの他のアプリケーションのWindowの表示を、自分のUE4プロジェクトのテクスチャとして扱うことができるようになります。

そのためメッシュに張り付けて3D空間に配置したり、UMG上のImageに張り付けることなんかができるようになります。

リアルタイムにスキャンしてテクスチャの中身を更新しているので、キャプチャー元のWindowの表示が更新されればテクスチャの表示も更新されます。

UE4のMedia PlayerとMedia Textureを使って動画を表示するのに近い使い勝手かと思います。

ブループリントOnlyのプロジェクトでも使用可能です。

詳しくは以前の記事を参照してください。

なぜ作ることにしたのか等書いています。

www.ayumax.net

UE4 マーケットプレイス

以下のページにて公開しています。

www.unrealengine.com

価格はFreeです。

Engineのバージョンには4.22に対応しています。(近いうちに4.20と4.21にも対応させます)

また、ソース一式も以下のGitHubにてMITライセンスで公開しています。

github.com

現状の機能とこれから

今(2019/7)のところウィンドウのキャプチャーのみ行えます。

今後はこれに操作機能(VRコントローラーなどでキャプチャーしたウィンドウを操作)を作ろうと考えています。

その他、今よりもパフォーマンスの改善や使い勝手の改善なども行っていく予定です。

特にキャプチャー対象のウィンドウの解像度が上がっていくと、処理負荷が高くなっていくのでその当たりが一番の課題かなと思っています。

こんな機能を追加してほしいなどのご要望ありましたら教えてください。

UE4 10km以上遠くにActorを移動すると破壊される件

UE4のWorldの大きさ問題

最近作ってたコンテンツでおきた問題をメモしておきます。

そのコンテンツはWheeledVehicleを使って車を走らせていたのですが、最近走るエリアを大幅に広げてみました。

それでちゃんと動くかテストしたところ、10kmを超えたあたりで車が消滅してしまいました。。。。

エリアが広すぎるせいで処理が重くなったりするかなあという不安はもっていたのですが、急に車が消滅するのは想像していませんでした。

そこで色々調べたところ原因と解決策が分かりました。

WorldSettingsのEnable World Bounds Checks

まず簡単にこの件を解決する方法としては、WorldSettingsのEnable World Bounds ChecksをOFFにすることです。

(色々検索しててみつけたのですが、元のページが見つからなくなってしまった。。。)

f:id:ayuma0913:20190703215908p:plain

この項目をOFFにするとたしかに10kmを超えても車は消滅しません。

なぜなのか?この項目は何に効いているのか?と疑問がでたのでもう少し調べてみました。

Editorでこの項目にマウスを合わせると以下の画像のようにツールチップがでます。

f:id:ayuma0913:20190703220325p:plain

ん?CheckStillInWorldってなんだ?と思ってEngineのソースを検索してみると、AActor::CheckStillInWorld‘という関数が見つかりました。これは怪しい。

bool AActor::CheckStillInWorld()
{
    if (IsPendingKill())
    {
        return false;
    }
    UWorld* MyWorld = GetWorld();
    if (!MyWorld)
    {
        return false;
    }

    // Only authority or non-networked actors should be destroyed, otherwise misprediction can destroy something the server is intending to keep alive.
    if (!(HasAuthority() || Role == ROLE_None))
    {
        return true;
    }

    // check the variations of KillZ
    AWorldSettings* WorldSettings = MyWorld->GetWorldSettings( true );

    if (!WorldSettings->bEnableWorldBoundsChecks)
    {
        return true;
    }

    if( GetActorLocation().Z < WorldSettings->KillZ )
    {
        UDamageType const* const DmgType = WorldSettings->KillZDamageType ? WorldSettings->KillZDamageType->GetDefaultObject<UDamageType>() : GetDefault<UDamageType>();
        FellOutOfWorld(*DmgType);
        return false;
    }
    // Check if box has poked outside the world
    else if( ( RootComponent != nullptr ) && ( GetRootComponent()->IsRegistered() == true ) )
    {
        const FBox&    Box = GetRootComponent()->Bounds.GetBox();
        if(   Box.Min.X < -HALF_WORLD_MAX || Box.Max.X > HALF_WORLD_MAX ||
            Box.Min.Y < -HALF_WORLD_MAX || Box.Max.Y > HALF_WORLD_MAX ||
            Box.Min.Z < -HALF_WORLD_MAX || Box.Max.Z > HALF_WORLD_MAX )
        {
            UE_LOG(LogActor, Warning, TEXT("%s is outside the world bounds!"), *GetName());
            OutsideWorldBounds();
            // not safe to use physics or collision at this point
            SetActorEnableCollision(false);
            DisableComponentsSimulatePhysics();
            return false;
        }
    }

    return true;
}

関数の一番最後に答えが書いてありました。

const FBox& Box = GetRootComponent()->Bounds.GetBox();
if(   Box.Min.X < -HALF_WORLD_MAX || Box.Max.X > HALF_WORLD_MAX ||
    Box.Min.Y < -HALF_WORLD_MAX || Box.Max.Y > HALF_WORLD_MAX ||
    Box.Min.Z < -HALF_WORLD_MAX || Box.Max.Z > HALF_WORLD_MAX )
{
        UE_LOG(LogActor, Warning, TEXT("%s is outside the world bounds!"), *GetName());
    OutsideWorldBounds();

ここでActorの位置のチェックが行われており、HALF_WORLD_MAXを超えた座標の場合はOutsideWorldBounds()が呼ばれています。

OutsideWorldBounds()の中を見ると`Destroy();‘とだけ書かれていました。

これやん!!!

HALF_WORLD_MAXの定義は以下のようになっています。

#define WORLD_MAX                   2097152.0              /* Maximum size of the world */
#define HALF_WORLD_MAX             (WORLD_MAX * 0.5)      /* Half the maximum size of the world */

あー、だいたい10kmだな。これで確定。

なので、設定をいじらない状態だと、-10kmから+10kmまでの全長20kmがとりえる最大の距離だということが分かりました。(対角線使えばもうちょっと長くできるけど)

これ以上広い世界を作るにはどうするんだろう?Enable World Bounds ChecksをOFFにするのはイレギュラーな気もするのでWorldを分けるとかするんでしょうか?気になるのでまた調べてみます。

ObjectDeliverer Ver 1.2.1リリース UDPReceiverの不具合修正

ObjectDelivererのVersion 1.2.1をリリース

今回はユーザーの方よりご指摘いただいた以下の不具合を修正しました。

  • UDPReceiverで既に使われているポートを指定するとEditorがクラッシュする
InnerSocket = FUdpSocketBuilder(TEXT("ObjectDeliverer UdpSocket"))
        .WithReceiveBufferSize(1024 * 1024)
        .BoundToPort(BoundPort)
        .Build();


Receiver = new FUdpSocketReceiver(InnerSocket, FTimespan::FromMilliseconds(10), TEXT("UProtocolUdpSocketReceiver"));
Receiver->OnDataReceived().BindUObject(this, &UProtocolUdpSocketReceiver::UdpReceivedCallback);
Receiver->Start();

if (InnerSocket)
{
    DispatchConnected(this);
}

上記が修正前のソースです。

BoundPortに既に使われているポート番号が指定されると、InnerSocketにはnullptrが入るためその後FUdpSocketReceiverを生成したタイミングでクラッシュしていました。

今回はこれを単純ですが以下のように修正しました。

InnerSocket = FUdpSocketBuilder(TEXT("ObjectDeliverer UdpSocket"))
        .WithReceiveBufferSize(1024 * 1024)
        .BoundToPort(BoundPort)
        .Build();

if (InnerSocket)
{
        Receiver = new FUdpSocketReceiver(InnerSocket, FTimespan::FromMilliseconds(10), TEXT("UProtocolUdpSocketReceiver"));
        Receiver->OnDataReceived().BindUObject(this, &UProtocolUdpSocketReceiver::UdpReceivedCallback);
        Receiver->Start();
    DispatchConnected(this);
}

これでFUdpSocketBuilderでの初期化に失敗した場合でも、クラッシュすることはなくなりました。

この不具合を見つけて、同じような仕組みのTCP/IP Serverは大丈夫かと思いましたが、そちらはガードが既にかかっており大丈夫でした。

単純なミスをやらかしてしまい、お恥ずかしいです。

UE4 WindowCapture機能をUMG対応しました

UMG対応

先日GitHubに公開しましたUE4向けのウィンドウキャプチャープラグインのWindowCapture2DにUMG対応を実施しました。

これで3D空間だけでなく、ウィンドウ内の固定位置にキャプチャー画像の貼り付けが可能になります。

使い方

UserWidgetに"WindowCaptureUMG"を配置します。

それだけです。

f:id:ayuma0913:20190621223142p:plain

WindowCaptureUMGに設定するプロパティはActor版のWindowCapturePlaneと同じものになるため、ActorでもWidgetでも同じ設定方法で使えます。

Editor Widgetへの応用

応用としてEditorWidgetにも使えます。

ただし、現状EditorWidgetに使用するとコンパイルボタンを押したときに起こる終了→再初期化の処理が通常のUMGとは異なるせいでクラッシュしてしまいます。。。

現在直し方を調整中です。

※ どうやらEditorWidgetを起動中にコンパイルすると落ちてしまうようです。一度EditorWidgetのウィンドウを閉じてからコンパイルすると落ちないことを確認しました。(2019/6/22)

UE4でウィンドウキャプチャー機能を作ってみた

WindowCapture2D

今回UnrealEngineで使えるウィンドウキャプチャープラグインをGitHub上に公開しました。

github.com

今はGitHubでのみ公開していますが、もう少し機能がまとまり次第マーケットプレイスにも出そうと思っています。

WindowCaptureへの思い

ある時にVRコンテンツをUE4でWindows上のアプリケーションをVR空間に表示したくて色々調べてたのですが、中々ダイレクトでマッチする機能は探せず。

当時はそんなに時間もなかったのでUE4で実現するのをあきらめていました。

そんな中ネットを見ているとUnityならドンピシャの機能を作られている方がいて、試してみると普通にやりたい事ができてしまい感動した記憶があります。

tips.hecomi.com

凄い凄いと思いながら、Unityでも実現できてるんだからUnrealでも作れるんじゃないだろうかと思い、トライしてみたのが始まりです。

Windowをキャプチャーする

私は25歳の時に今で言う「未経験からエンジニア」で転職したのですが、その時最初にやった業務がWindows上でのウィンドウキャプチャーでした。

C++, MFCで戦っていたのですが、当時はHDC? HWND?何それ?メモリDCってなに?って感じだったのを覚えています。

そのためウィンドウをキャプチャーするという技術には特別な思いがあります。

キャプチャー方法

私の知っているウィンドウキャプチャー方法は以下の2つです。

  • WindowのDCを取得してBitBlt
  • WindowのDCを取得してPrintWindow

速度的にはBitBltの方がPrintWindowよりも早くて良いのですが、ChromeやUE4で作ったアプリケーションなどはBitBltではキャプチャーできません。

Windowを直接キャプチャーするのではなく、デスクトップのDCからキャプチャーして必要な領域をトリミングすると、どんなウィンドウでもキャプチャーできるのですが、 キャプチャーするのにディスプレイモニターのリフレッシュレート分待たされてしまう問題や、重なったウィンドウの背面側はキャプチャーできない等問題も数多くありました。

もう一つのPrintWindowも当初はChromeのキャプチャーはできないと思っていたのですが、Microsoftのヘルプに載っていない引数(PW_RENDERFULLCONTENT=2)を与えるとキャプチャーできるようになる発見がありました。

そこで今回はPrintWindowで作ってみる事にしました。

(その他にDirectXを使ってウィンドウキャプチャーする方法もあるそうですが、これはまだ試していません)

UE4での実装

とりあえずTickの中でターゲットのウィンドウをキャプチャーしてテクスチャの中身を書き換えてみました。

テクスチャー(UTexture2D)の書き換えは以下のコードで実現できます。

auto Region = new FUpdateTextureRegion2D(0, 0, 0, 0, TextureTarget->GetSizeX(), TextureTarget->GetSizeY());
TextureTarget->UpdateTextureRegions(0, 1, Region, 4 * TextureTarget->GetSizeX(), 4, (uint8*)m_BitmapBuffer);

TextureTargetがターゲットとなるTexture2Dで、 m_BitmapBufferはBGRA形式のピクセルバッファです。

この2行で動的にテクスチャーの書き換えができます。

実行するとうまくいったかのように見えたのですが、対象のウィンドウのサイズが大きくなるとカクツキが酷くなってしまい困りました。

そこで次はキャプチャーの部分のみTaskを使ってバックグラウンドで処理してみました。

void ACaptureMachine::Tick()
{
    AsyncTask(ENamedThreads::BackgroundThreadPriority, [this]() {
        // キャプチャー処理を実施
        });
}

そうすると先ほどよりもフレームレートが上がり効果がありましたので、次に複数ウィンドウのキャプチャーにトライしました。

3枚のウィンドウを同時にキャプチャーしたところ、またまた極端にフレームレートが落ちてしまい困ります。

考えてみると、このウィンドウキャプチャーは1個のActorで1つのウィンドウをキャプチャーしており、Tick内で処理している以上直列に処理が繋がってしまいどんどん遅くなっているのではと仮説をたてました。

なのでTick内で処理するのはやめて、Actor内でスレッドを生成しスレッド内でキャプチャーをするように改造しました。

すると。。。

ちょいミスもありましたが、3枚のウィンドウをキャプチャーしても60fpsキープできるようになりました。

もっとウィンドウの枚数を増やしたり、4Kウィンドウフルサイズなどウィンドウを大きくしていくと負荷は高まっていくとは思いますが、とりあえずは最低限の機能は実装できたかなと思っています。

使い方

使い方は以下の通りです。(今後は仕様変更により使い方が変わる可能性はあります)

  1. 「WindowCapturePlane」アクターをレベルに置く。 image

  2. 置いたアクターのプロパティを設定する。

image

プロパティ 説明
Capture Target Title キャプチャー対象のウィンドウタイトル文字列を入力
Title Matching Window Search Capture Target Titleに入れた文字列とウィンドウタイトルのマッチング方法を選択
Frame Rate キャプチャーレート (fps)
Check Window Size 有効にするとウィンドウのサイズ変更に追従しますが、処理負荷が高くなります
Cut Shadow ウィンドウの影を除いてキャプチャーします

Title Matching Window Search

説明
PerfectMatch 入力文字列との完全一致 
ForwardMatch 前方一致で比較
PartialMatch 部分一致で比較
BackwardMatch 後方一致で比較
RegularExpression 正規表現を使って比較

以上でPlayすると設定がうまくいけばウィンドウがキャプチャーされます。

ただし以下の点にはお気を付けください。

※1 現状ウィンドウの探索は起動直後に1回のみのため、対象のウィンドウが表示されている状態でUE4側をPlayする必要があります

※2 対象のウィンドウを最小化してしまうとキャプチャーできなくなります

今後の展開

現状ウィンドウをキャプチャーすることはできるようになりましたが、表示のみで操作ができません。

例えばVRで使うことを考えたときに、空間内でウィンドウを操作できた方が使い道が広がると思うので、是非ウィンドウの操作(マウスやキーボード入力の転送)も実装してみたいと思います。

あとはネットワーク共有とかもやってみたいけど、これは難しそうなので断念するかもです。