読者です 読者をやめる 読者になる 読者になる

catalinaの備忘録

ソフトウェアやハードウェアの備忘録。後で逆引きできるように。

Hololensでビルボードを表示する

UWP/c++ UWP/c++ DirectX11 hololens VisualStudio2015

アプリをごりごり作るよりも前にデバッグ情報などを実機で見やすくする仕組みをつくりました。かたりぃなです。

結論

従来手法のビルボードはHoloLensではそのまま使うのは少し難ありです。HoloLensが提供している機能を使ってビルボードレンダリングするのが良い。

ビルボードって?

この用語が本当に正しいのかどうか疑問ではありますが、概要だけ。 一言で言うなら「常にカメラのほうを向くポリゴン」です。

どういうところで使うの?

3Dのゲームを遊んだことがある人なら一度は見かけたことがあると思います。 「常にカメラのほうを向いていなければいけない」ポリゴンというのは多々あって、例えば次のようなものが考えられます。

  • MMORPGなどでのキャラクタ名、ダメージ数字の表示、メニュー、ツールバーなど
  • FPSの照準アイコン
  • ポイントスプライト(パーティクル)

これらは一枚の板ポリゴンにテクスチャを張って、そのポリゴンが必ずカメラのほうを向くように設定することで実現できます。

どうやって実現するの?

カメラのほうを向くポリゴンを作る方法は、まず最も単純な方法としてはカメラ座標系でX,Y平面にポリゴンを書くだけで実現できると思います。 先の例でのメニューやツールバーなどはこれで解決ですが、応用してパーティクルを使った演出をする場合にちょっと不便で汎用性に欠けます。

というのもパーティクルはワールド空間上の特定の位置に配置されるオブジェクト(火の粉だったり、煙だったりのエフェクト)であるので、ワールド座標系で指定したいものです。

こうして考えると、カメラ座標系で直接ポリゴンを描くのではなく、ワールド座標系で座標を指定したうえでポリゴンが常にカメラのほうを向くようにしたいです。

3Dゲームなんか向けのサイトを見てみると、カメラの回転行列からの逆行列をオブジェクトの行列の回転行列成分にセットするという方法が一般的のようです。 というわけで、HoloLensのカメラ行列からビルボードを作れないか試してみます。

HoloLensのレンダリングパイプライン

カメラ行列からの逆行列を求めてそれを使うというアプローチですが、HoloLensで同様の手法を使うことはできなさそうです。 3Dモデルをレンダリングするとき、のカメラ行列の設定ですが、VisualStudioが生成するスケルトンではこうなっていました。

// DX::CameraResources::UpdateViewProjectionBuffer関数
    DX::ViewProjectionConstantBuffer viewProjectionConstantBufferData;
    bool viewTransformAcquired = viewTransformContainer != nullptr;
    if (viewTransformAcquired)
    {
        // Otherwise, the set of view transforms can be retrieved.
        HolographicStereoTransform viewCoordinateSystemTransform = viewTransformContainer->Value;

        viewProjectionConstantBufferData.viewProjection[0] = viewCoordinateSystemTransform.Left * cameraProjectionTransform.Left;
        viewProjectionConstantBufferData.viewProjection[1] = viewCoordinateSystemTransform.Right * cameraProjectionTransform.Right;
    }
// 以降、viewProjectionConstantBufferDataをUpdateSubresourceでVertexShaderのconstant bufferに転送する

constant bufferを参照するHLSLシェーダはこうなっています。

cbuffer ViewProjectionConstantBuffer : register(b1)
{
    float4x4 viewProjection[2];
};
// 略
VertexShaderOutput main(VertexShaderInput input){
    // 略
    // Correct for perspective and project the vertex position onto the screen.
    pos = mul(pos, viewProjection[idx]);
    output.pos = (min16float4)pos;

何やってるのかの前にAR/VRの基本的な話です。

HMDを使って3Dレンダリングするとき、左右のレンズに少しだけ異なる映像を出すことで立体感を出すという手法があります。(=ステレオグラム) 人間の目が少し離れた位置についていて~と話が長くなりそうなので、ここでは「そういう手法がある」程度にとどめておきます。

HoloLensも同じ理屈で、左右のレンズに同じものをレンダリングしているわけではなく、左レンズ用と右レンズ用の映像をレンダリングします。 HolographicStereoTransformというクラスが左レンズと右レンズそれぞれの透視投影変換とビュー行列をもっているので、これを引っ張ってくるわけですね。

それぞれのレンズ用にレンダリングするためにDrawIndexedInstancedを使って左レンズ用インスタンスと右レンズ用インスタンスレンダリングします。 テンプレートプロジェクトではシェーダーの入力パラメータにinstIdというのがいますが、これがインスタンスのIDというわけですね。

HolographicStereoTransform struct - UWP app developer | Microsoft Docs

なので、従来手法の「カメラの回転要素の逆行列を3Dモデルの回転行列に使う」というアプローチをそのまま流用しようとしても、モデルの回転行列を左右レンズそれぞれに生成する必要があって、それはあんまりです。

HoloLensの座標系で考える

そもそもHoloLensではスクリーン中央に常にカーソルがレンダリングされています。 このカーソルは視線の先の空間マッピングされたオブジェクト上に出るのですが、これと同じことができればいいわけです。

公式マニュアルに座標系のことが書いてありました。

Locatable camera

これを真似していけば良さそうです。というかできました。

バイスの相対座標と絶対座標

絶対座標と相対座標というと誤解しそうですが、概念として

  • 絶対座標:空間マッピングしたワールド空間上の座標を表す
  • 相対座標:デバイスそのものからの座標を表す

と解釈できます。 HoloLensのプロモーションなどで空間上に何かのオブジェクトを配置しているケースでは上記の絶対座標を用います。 相対座標はドラゴンボールスカウター名探偵コナンの探偵メガネのように装着者からみた座標系として考えられます。

バイス相対座標を取得する

まず、VisualStudioのHoloLensテンプレートプロジェクトでは、座標系として絶対座標(ワールド座標系)を使っています。 Windows::Perception::Spatial::SpatialStationaryFrameOfReferenceがそれです。

上記リンク先の記事を読みつつ、相対座標系に置き換えます。 Windows::Perception::Spatial::SpatialLocatorAttachedFrameOfReferenceがデバイスからの相対座標を扱うための参照です。

型を置き換えてもこのままではインタフェースが違うのでうまくいきません。 SpatialLocatorAttachedFrameOfReferenceでSpatialCoordinateSystem^を拾うには

GetStationaryCoordinateSystemAtTimestamp(prediction->Timestamp);

とします。

HoloLensの姿勢を取得する

これでHoloLensの相対座標系で色々できるようになりました。 姿勢(視線の向き)などは

SpatialPointerPose^ pointerPose = SpatialPointerPose::TryGetAtTimestamp(currentCoordinateSystem, prediction->Timestamp);

で拾えます。

公式マニュアルはこちら。

SpatialPointerPose class - UWP app developer | Microsoft Docs

pointerPoseからは次のようにして頭の位置、向きなどが拾えるようです。

pointerPose->Head->Position
pointerPose->Head->ForwardDirection
// etc.

あとはマニュアルどおりやればビルボードが出ました。

手っ取り早く解決したい(サンプルコードを持ってくる)

手っ取り早くサンプルコードをコピペして済ませる方法もありました。

https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/HolographicFaceTracking

HoloLens実機ない人のために動作概要の解説

  • HoloLensのカメラで実世界のカメラ映像を取得(media capture)
  • Microsoftの顔認識APIに映像を入力
  • 顔として認識できる領域があれば、そこを切り取ってDirectXテクスチャを作る
  • 切り取った顔領域をビルボードとして画面中央より少し左よりの位置にレンダリング
  • 顔領域と思われる領域の空間の少し上部に3Dモデルをレンダリング
  • 文字列をテクスチャとして生成する(direct2d)
  • 顔が認識できないもしくはカメラがない(エミュレータ)の場合は前述の文字列を書き込んだテクスチャをビルボードレンダリング

です。 顔検出は置いといて、エミュレータで動かしても「no avalilavle camera」のビルボードは出るので、必要箇所を抜粋すれば良いかと。 試しに該当する機能を持ってきてみると、期待通りビルボードが表示できました。

ちょっと詰まったポイントがあったので2点ほど。

以下、サンプルコード引用時のトラブルシューティングです。

サンプルコード引用したシェーダーのコンパイルに失敗する

GeometryShaderを新たに作ってVisualStudioでコンパイルすると、こんなエラーが出ます。

warning X3554: unknown attribute maxvertexcount, or attribute invalid for this statement
error X3514: 'main': input parameter 'input' cannot have a geometry specifier

これはVisualStudioプロジェクトに後から追加したhlslファイルはコンパイラの設定が不足していることによるものです。 maxvertexcountはgeometryShaderでのみ使えるアトリビュートなので、コンパイラに「これはgeometryShaderですよ」と教えてあげる必要があります。

新しいhlslファイルをソシューションに組み込んだ直後の設定はこうなっています。 この図ではシェーダーの種類が指定されていません。何シェーダを作るつもりなんでしょうね。 f:id:Catalina1344:20170319123008p:plain

例えばGeometryShaderならこういう形で指定します。pixelShaderやVertexShaderも同様。 シェーダーレベルは環境に合わせて適切なものを選択します。 f:id:Catalina1344:20170319123011p:plain

サンプルコード引用したが何も表示されない

VisualStudio2015の生成するHoloLens用Direct3Dのテンプレートと、上記サンプルコードでは、行列の型が違っています。

テンプレートではDirectX::XMFLOAT4X4を行列の型として使っていますが、サンプルコードではWindows::Foundation::Numerics::float4x4です。 全体で統一が取れていればいいので、どちらかに合わせてしまいましょう。

たぶん内部構造が少し違うのかなと推測していますが、追うの面倒なので「やっぱ型は合わせたほうがいいよね」程度で。

文字列をテクスチャとして生成できているか謎

そもそも基本的な機能が動いていないとお話になりませんよね。 例えばテクスチャがすべて黒だった(=何も表示されていないように見えていただけ)なんて悲しい思いはしたくないものです。

テクスチャが生成されていない/テクスチャの参照が失敗しているかもと不安な場合は、pixelshaderでテクスチャサンプラを使わずに単色出力を試すと幸せになれます。

たとえば

//   return min16float4(rgbChannel.Sample(defaultSampler, input.texCoord));
    return float4(1.f, 1.f, 1.f, 1.f);  // RGBA . Aは0に近いほど透明. 1は不透過

で、白色単色のビルボードが出るので、最初はこうしておけば問題切り分けに役立ちます。

感想と今後の展望

とりあえずビルボードレンダリングできるようになりました。 HoloLensの座標系も少し詳しくなりました。

あとはARやるための仕組みをごりごりと書いていきたいところですが、 もう少しコード整理して、いつでもデバッグ用に組み込める形にしてから次のステップに移りたいと思います。

それでは今回はこれにて。

HololensでMediaCaptureを使ってカメラプレビューを取得する

C++ OpenCV hololens UWP

HoloLensで入力画像を処理する前準備をしました。かたりぃなです。

今回やること

今回の記事でやることをざっと列挙します。

  • UWPでMediaCaptureを叩いてプレビューフレームを取得する
  • OpenCVのMat形式で扱えるようにする
  • とりあえず画像処理する

です。 画像処理の結果を使って3Dレンダリングまでやりたかったのですが、ちょっと詰まってしまっているのでいったんここまで記事にしておきます。 シミュレータではカメラが使えないのでほとんどが実機デバッグになります。買ってよかったHoloLens。

カメラから映像を取得する

HoloLensでMediaCaptureを使ってプレビューフレームを取得するコードはMicrosoft公式がサンプルコードを出してくれているのでそのまま使います。 中身ざっと眺めたところ、CreateAsyncしてGetLatestFrameするだけで使えるみたいです。

https://github.com/Microsoft/Windows-universal-samples/blob/master/Samples/HolographicFaceTracking/cpp/Content/VideoFrameProcessor.cpp

早速コード。 アプリのメインクラスの定義にメンバ変数を作っておきます。 mediacaptureを制御するクラスと、プレビューフレームを処理するワーカースレッドです。

    std::shared_ptr<HolographicFaceTracker::VideoFrameProcessor>    m_videoProcessor;
    std::thread                                                     m_worker_thread;

CreateAsyncでインスタンス化したものを保持しておき、std::threadでぐるぐる回します。 取得した映像フレームを処理するVideoProcessingという関数を作りました。ここで画像処理をします。

    task<void> videoInitTask = HolographicFaceTracker::VideoFrameProcessor::CreateAsync()
    .then([this](std::shared_ptr<HolographicFaceTracker::VideoFrameProcessor> videoProcessor)
    {
        m_videoProcessor = std::move(videoProcessor);
        m_worker_thread = std::thread([this]{
            while (1) {
                if (m_videoProcessor) {
                    auto frame = this->m_videoProcessor->GetLatestFrame();
                    if (frame) {
                        if (Windows::Media::Capture::Frames::VideoMediaFrame^ videoMediaFrame = frame->VideoMediaFrame) {
                            VideoProcessing(videoMediaFrame);
                        }
                    }
                }
            }
        });
    });

OpenCVに渡す(前準備)

映像フレームをOpenCVで処理します。 その前にOpenCVをUWP向けにビルドしておいたライブラリをプロジェクトに組み込みます。 MSがUWP向けに準備してくれているOpenCVはこちら。 GitHub - Microsoft/opencv: Open Source Computer Vision Library

試しに組み込んだときの記事はこちら。 WindowsストアアプリでOpenCVを使う - catalinaの備忘録

というわけで同様にして組み込みます。

先ほどのサンプルコード見るとそのまま画素にアクセスできるらしいので真似します。

 auto buffer = frame->SoftwareBitmap->LockBuffer(Windows::Graphics::Imaging::BitmapBufferAccessMode::Read);
    IMemoryBufferReference^ bufferRef = buffer->CreateReference();

    Microsoft::WRL::ComPtr<Windows::Foundation::IMemoryBufferByteAccess> memoryBufferByteAccess;
    if (SUCCEEDED(reinterpret_cast<IInspectable*>(bufferRef)->QueryInterface(IID_PPV_ARGS(&memoryBufferByteAccess))))
    {
        BYTE* pSourceBuffer = nullptr;
        UINT32 sourceCapacity = 0;
        if (SUCCEEDED(memoryBufferByteAccess->GetBuffer(&pSourceBuffer, &sourceCapacity)) && pSourceBuffer)
        {
            // pSourceBuffer使ってごにょごにょする
            // YUV420Pとして扱えばいい
        }
    }

ここで注意として、今回のコードではbitmapはNv12フォーマットです。 NV12とは。。。 YUV Video Subtypes (Windows) とのことで、いわゆるYUV420P形式です。

プレビューフレームの画素のバイト列をOpenCVのcv::Matにする。

簡単に。 OpenCVのMatとして取り扱います。YUV420Pなので最初のプレーンを参照すれば輝度のみ取れるので、グレースケール画像として取り扱えます。

auto cv_preview_image = cv::Mat(cv::Size(frame->SoftwareBitmap->PixelWidth, frame->SoftwareBitmap->PixelHeight), CV_8UC1, pSourceBuffer);

これでOKです。 cv::Matのコンストラクタは色々ありますが、この形式のものを使います。 第一引数はcv::Sizeであらわされる画像の幅と高さ。 第二引数はピクセルフォーマット(厳密にはmatの各要素のtype)で、pSourceBufferが画素列の先頭アドレスです。

デバッガで目視確認する

ピクセルの数列見ても楽しくないので、画を見たいです。 MS公式がVisualStudioのプラグインとしてcv::Matのイメージビューワを提供してくれてるので使います。 Image Watch - Visual Studio Marketplace

適当にフィルタ処理してみるとそれっぽくできてるのが確認できました。 というわけで画像処理の入り口まで辿り着きました。

課題・今後の展望

HoloLens実機でのデバッグはそれなりに手間がかかります。デバッガで少し見て解る問題ならいいんですが、ちょっと複雑なことをやろうとすると、厳しいです。

特に問題になるのは、リアルタイム処理とVisualStudioのデバッガが相性悪すぎといったところです。 映像をリアルタイムに処理した結果をデバッガのログに出していても、それを見るために頭を動かしてしまうと状況が変わってしまいます。 こればっかりはどうしようもないので、HoloLens側でデバッグ情報をレンダリングする仕組みを作っておいたほうが後々幸せになれそうな気がします。

とりあえずビルボードでテキストをレンダリングする仕組みくらい作っておいて損はなさそうです。

では今回はこれにて。

HoloLensでChainerの学習経過を覗いてみた

C++ DirectX11 hololens Python Chainer

せっかくHoloLensがあるので機械学習の途中経過を覗いて遊んでみました。かたりぃなです。

学習中のデータの可視化する意義

機械学習についてですが、Chainerなどのフレームワークがどうやって学習を進めているかよくわかっていません。 具体的にはフレームワークによってネットワーク内パラメータがどう変動していくのかが分からないのです。 optimizerがいて順伝播と逆伝播の誤差を~というのは理屈ではそうなのでしょうけれども、実際に中を見ていないのでしっくりきていません。

Chainer自体はよくできたフレームワークなのでexampleの中身動かして「あ、こんな簡単に動くんだね」と感動するのですが、そこから先どうしようかという展望を持ちにくい印象です。 学習が進むことによって「ニューラルネットワークのパラメータにどういう"変化"が起きているのか」を見れたらもっと欲望というか展望が見える気がします。

可視化といえばchainerが出しているログをグラフにしたり、各層を可視化するなどといった手法はありますが、それって静的なものなので、「どういう過程で生み出されたものなのか」が見えにくいです。 少しずつ変化していっているとしても、やっぱりそこを見てみたいですよね。 可視化した画像をブラウザで開いてCtrl+F5連打という力業を試しましたが毎回連打するのは疲れます。

というわけで、機械学習の進行状況をリアルタイムで見れるようにアプリを作ってみます。 せっかくHoloLensあるのでこいつを使います。

実運用ではオーバーヘッドが大きすぎ(GPUからCPUにNNを取り出してからnpz形式でファイル出力)で役に立たないとは思いますが、まずは動いているものを見て興味を持つという点が重要かなと思っています。

結果

こんなの出ました。一定周期で更新されるワイヤーフレームはまるで昭和時代のCGです。 だんだんと山と谷の差が大きくなっていく様子が観察できました。

f:id:Catalina1344:20170218215123p:plain

実世界のテーブルの上とかに表示しておいて山の裏側を見たいってときは回り込めば見えます。 (実機でのキャプチャしようと思ったのですがDirectXレンダリング結果をオーバーレイしたキャプチャがうまくできないので保留です)

やったこと、やらなかったこと

今回やってみたことと、保留にしたものを列挙します。 大まかに考えると、「可視化すること」が目標で、そこから洗い出された課題などはいったん保留です。

やったこと

  • chainerのネットワークの状態を一定周期でファイルに出力する
  • ネットワークの種類はautoencoder(可視化して結果がわかりやすそう)
  • HoloLens側からファイルをポーリングして一定周期で読み出す
  • HoloLens上でDirectXを使ってレンダリング

やらなかったこと

  • 機械学習の分野の詳細に立ち入ること(学習が収束するとか、効率の良いネットワークだとか)
  • 可視化した結果の正確性
  • 過度な高速化(HoloLensでとりあえず表示できればいい)

以降、実装の詳細です。

chainerのサンプルコードを編集

chainerはPC上で実行します。 まずchainerのmnistサンプルコードでスナップショットを出力する部分がありますが、ここを変更します。

trainer.extend(extensions.snapshot(filename='work.npz'), trigger=(1, 'epoch'))

デフォルトでは連番ファイルを出力するようになっているところを変更します。同一ファイルをwrite/readし続けたほうが実験としてはやりやすいので。 また、トリガは毎epoch終了ごととします。 ネットワークが大きいくなると毎epochやっていると重そうですが、今は気にしないことにします。

次にネットワークをautoencoderにします。 autoencoderを選んだ理由は実装が簡単かつ「なんかそれっぽい気がする」ものが見えそうだからです。 データセットにmnistを使うので入力/出力ともに784次元のデータです。

class MLP(chainer.Chain):
    def __init__(self, n_units):
        super(MLP, self).__init__(
            # the size of the inputs to each layer will be inferred
            encoder = L.Linear(None, n_units),  # n_in -> n_units
            decoder = L.Linear(None, 784)
        )

    def __call__(self, x):
        h = F.relu(self.encoder(x))
        return self.decoder(h)

ここでencoder/decoderと名前を付けましたが、この名前がそのままスナップショット内のネットワークのレイヤ名に使われるので、わかりやすい名前をつけておいたほうが幸せになれます。

最後にデータセットを入出力ともに同じものを使うようにします。 withlabelをfalseにすれば教師ラベルなしの入力データがそのまま取得できます。 ただし、学習のイテレーションでは教師データをTupleDatasetとして与える必要があるので、 入力=出力となるDatasetを生成します。testも同様です。

    train, test = chainer.datasets.get_mnist(withlabel=False)
    train = tuple_dataset.TupleDataset(train, train)
    test = tuple_dataset.TupleDataset(test, test)

ちょっと寄り道して動作確認。 それっぽく動いているかどうかpythonから画像を出力して確認してみます。 plot関係のライブラリを使って画像に出します。

なんか見れた。 f:id:Catalina1344:20170218151616p:plain

寄り道の詳細

ClassifierしたmodelからだとNNを参照するのが大変そう(簡単にできるのだろうか?)なので、networkをとっておいてそこから重み係数を参照することにしました。 また画像として出力するにはユニット数が少ないほうが出力画像に収めやすいので10x10枚の可視化=100ユニットとしています。

import matplotlib.pyplot as plt
import numpy as np

#~~~
network = MLP(unit)
model = L.Classifier(network)

#~~~
save_images(network.encoder.data, "plot_test.png")

#~~~
# 画像で保存(隠れ層=100という前提)
def save_images(x, filename):
    fig, ax = plt.subplots(10, 10, figsize=(10, 10), dpi=100)
    for ai, xi in zip(ax.flatten(), x):
        ai.imshow(xi.reshape(28, 28))
    fig.savefig(filename)

本当にこれでいいのか不安ですが、これでchainer側で可視化の準備は整いました。 スナップショットはresultフォルダに出力されます。 ついでにdotファイル吐かせてgraphvizで見れることも確認しました。

chainerのスナップショットをhttpで取得する

まずHTTPサーバをpythonで簡単に立ち上げます。3.5系ではこうなります。

python -m http.server

これで8000番ポートが開くので、httpアクセスすると上記スクリプトを実行したディレクトリに置かれたファイルを参照できます。

次はHoloLensアプリ側の実装です。 と、その前にHoloLensの設定とリモートデバッガの設定をします。

HoloLensの設定

HoloLensのデバイスポータルに接続してデバイスの情報を拾えるようにしておきます。

これをやっておけばPCのブラウザからHoloLensにアクセスできるようになります。 証明書のインストールとかあるので、手順は公式を参照。

HoloLens 用 Device Portal | Microsoft Docs

リモートデバッガを構成する

VisualStudioのデバッグ構成を変更してリモートデバッグの設定をします。 とはいえ毎回IP調べるのは面倒なので固定IPにしたほうが便利です。 f:id:Catalina1344:20170214214457p:plain

HTTPサーバからファイルを取得する

ここからHoloLens側の実装です。 VisualStudio2015のテンプレートからHoloLens DirectX11を使って作業していきます。

httpアクセスしてchainerの学習中スナップショットを取得します。 UWPのhttpclientクラスはキャッシュが効いてしまって実験中は不便なのでHttpCacheReadBehavior::MostRecentを設定します。 あとは普通にresponseを見てdatareadすれば済みます。 エラーチェックしていませんが、ストアに提出するようなアプリのときはもうちょっと真面目にやりましょう。

     auto uri = ref new Windows::Foundation::Uri(L"http://192.168.0.14:8000/result/work.npz";);
        auto filter = ref new Windows::Web::Http::Filters::HttpBaseProtocolFilter();
        filter->CacheControl->ReadBehavior = Windows::Web::Http::Filters::HttpCacheReadBehavior::MostRecent;
        auto client = ref new Windows::Web::Http::HttpClient(filter);
        auto headers = client->DefaultRequestHeaders;
        create_task(client->GetAsync(uri)).then([cb](Windows::Web::Http::HttpResponseMessage ^ response) {
            response->EnsureSuccessStatusCode();
            return response->Content->ReadAsBufferAsync();
        }).then([cb](Windows::Storage::Streams::IBuffer ^ input) {
            auto reader = Windows::Storage::Streams::DataReader::FromBuffer(input);
            auto loaded_buffer = reader->ReadBuffer(input->Length);
            parse_npz(loaded_buffer, cb);
        });

この処理を一定周期(1秒間隔とか)で呼び出してあげたうえでzip形式を解釈してzlibで伸長してあげれば完成です。

Direct3Dレンダリングする

取得したNNのencoderレイヤの各ユニットの重み係数は28x28のシングルチャンネル画像として解釈できます。 今回は単純にW係数を高さ(Y)にとることにします。(つまり、X,Zは0~27の間のいずれかの値で、X,Zが与えられるとYが一意に決まる,Yは重み係数) ポリゴンレンダリングすると色々と大変なのでワイヤーフレームでいきます。ワイヤーフレームのほうが味がありますし。

ラスタライズステージでワイヤーフレームにします。

 D3D11_RASTERIZER_DESC rasterizerDesc = {
        D3D11_FILL_WIREFRAME,   // ワイヤフレーム
        D3D11_CULL_NONE,        // カリングなし
        FALSE,
        0,
        0.0f,
        FALSE,
        FALSE,
        FALSE,
        FALSE,
        FALSE
    };
    ID3D11RasterizerState* rasterizerState = NULL;
    HRESULT hr = m_deviceResources->GetD3DDevice()->CreateRasterizerState(&rasterizerDesc, &rasterizerState);
    if (FAILED(hr)) {
        // TODO : エラー処理
    }
    context->RSSetState(rasterizerState);

しかしこれではやりたいことに対して少し不足しています。 VisualStudioのDirectXテンプレートではTriangleListをシェーダーに入力していますが単純にLineListを入力したいところです。

GeometryShaderを書く

LineListをシェーダに入力するには、まずCPU側のコードは単純に

context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINELIST);

すれば良いです。

これに合わせてGeometryShadeerのコードを変更します。 3頂点を受け取ってに三角形を吐き出すのではなく、2頂点を受け取って1つのLine(始点,終点)を出力するよう書き換えます。

-[maxvertexcount(3)]
-void main(triangle GeometryShaderInput input[3], inout TriangleStream<GeometryShaderOutput> outStream)
+[maxvertexcount(2)]
+void main(line GeometryShaderInput input[2], inout LineStream<GeometryShaderOutput> outStream)

シェーダー内でやっているジオメトリ生成も同様にして2頂点ずつ生成するようにします。

    [unroll(2)]
    for (int i = 0; i < 2; ++i)
    {
        // 略
    }

あとはLineListの規則に従って頂点バッファとインデックスバッファを定義して格子状のメッシュを生成すれば完成です。

頂点バッファを動的に更新する

表示するワイヤーフレームができたので、Y軸を動的に更新します。 乱暴ですが新しい頂点情報を受け取るたびにVertexBufferを作り直すことにしました。 UpdateSubResourceのほうが適任な気がしますが、HoloLensのサンプルコードのspatialmappingもこういう実装になっていたりするのでいったん良しとします。 コードは自分自身納得いっていないので省略です。

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

chainerのモデルをC++から読み込むのはこちらのサイトが参考になりました。そのままnpzを読み込めました。 ChainerのモデルをC++で読み込む - TadaoYamaokaの日記

感想

chainerのモデルをC++から読みだすことができるようになりました。

「なんとなくdeepLearningやってる」感じが見えたので良しとします。 できれば1つのレイヤ内のすべてのユニットを可視化したかったのですが、視界からはみ出して残念な見栄えになるので諦めました。

とはいえChainerに慣れていない段階だと便利な気がします。HoloLens装着したまま別の作業進めつつ、進捗状況をチラ見できます。 PCの画面を占有しない+進捗確認の作業が単純(頭の向きを変えるだけ)っていうのは正義ですね。

しかしながらワイヤーフレーム表示では学習が進んでデータのレンジが大きくなってきたときに辛いです。実世界に表示したモデルをぐるっと回って見るのであれば光源処理を入れたポリゴンのほうがいい気がします。

あと今回の簡易的な実装ではHoloLens側の処理がやたら重いです。 どれくらい重いかというとHoloLensのジェスチャ操作が効きにくくなるくらい重いです。

原因は詳しく調べてはいませんが、Chainerのsnapshotのnpzを展開するとき何も考えずに全部展開してしまっているのが原因かもしれないと思っています。というのも、snapshotで保存されるnpzはすべての情報が含まれていて、今回使わないdecoderやoptimizerなんかも展開してしまっているので。。。 気が向いたときに調べてみます。

今後の展望

読みだした学習済みモデルを食べさせるC++実装があれば色々できそうです。 あとはdotファイルをうまく解析すれば「c++からchainerのモデルなんでも読めるよ」という夢も叶うかもしれませんね。 書いてて気づきましたがboost::proptreeとcv::dnn使えばできそうな気がします。

アプリづくりの練習としてChainerの出力したdotファイルを読んで、どの層を見るかを選択するUIつけてあげれば面白そうです。

それでは今回はこれにて。

Hololensでカメラを扱う[c++/UWP]

hololens C++ UWP

HoloLensにはカメラが搭載されているので、どんなもんが載ってるか調べてみようと思います。かたりぃなです。

(2017/02/26更新) Hardware detailsのURLを追記。

HoloLensのカメラって?

公式サイト

https://developer.microsoft.com/en-us/windows/holographic/hardware_details#pagenotfound

ここからUnderstanding HoloLens->Hardware detailsで詳細を見れますが現時点ではリンク切れになってしまっています。(2017/02/09 17:00現在) まあ、無いものねだりしても仕方ないので、そのうち復旧することを祈りつつソフトウェア側から攻めることにします。

(2017/02/26更新) URL更新されているとのコメントいただきました。現在はHoloLens hardware detailsでアクセスできました。

結論

手元のHoloLens上でデバイスを列挙してプレビューの開始をしてみたところ、"MN34150"という型番のデバイスが1個だけ列挙されました。

型番調べてみたところ、これっぽいです。 MN34150 | 製品 | 半導体 | Panasonic

列挙されたデバイスの物理的な場所はHololens正面の鼻あての上あたりでした。(カメラの前に手をかざしてプレビュー見ながら確認)

UWPでのカメラ操作

まずUWPでのカメラAPIは2種類存在しています。

  1. CameraCaptureUI
  2. MediaCapture

1はカメラを操作するUIも含めて提供されている高レベルAPIです。 インスタンス作って呼びだせば写真撮影のUIが起動します。

2は1と比べると低レベルなAPIで、デバイスの列挙、カメラの準備、プレビュー開始、撮影など細かく制御できます。 中身はMediaFoundationらしく、MediaTransferのインターフェイスをもったエフェクトの挿入などもできるようです。 今回はこれを使ってHoloLensのデバイスを列挙してみます。

詳細なドキュメントはそれぞれ以下になります。

MediaCapture class - UWP app developer | Microsoft Docs

CameraCaptureUI Class (Windows)

DeviceInformationを使ってカメラデバイスを列挙する

まずMainPageのクラス定義に以下を追加します。列挙されたデバイスを保持しておくためのものです。

DeviceInformationCollection^ m_devInfoCollection;

そして初期化コードに以下のものを埋め込みます。ほとんどサンプルコードのままです。

いつもどおりcreate_taskで生成したタスクの実行が完了すると、then節のラムダが起動します。ラムダ内で列挙されたデバイスをごにょごにょします。 このAPIは列挙されたデバイス個別にコールバックするのではなくまとめてコレクション返してくる仕様のようです。

 m_devInfoCollection = nullptr;

    EnumedDeviceList2->Items->Clear();

    create_task(DeviceInformation::FindAllAsync(DeviceClass::VideoCapture)).then([this](task<DeviceInformationCollection^> findTask)
    {
        m_devInfoCollection = findTask.get();
        for (unsigned int i = 0; i < m_devInfoCollection->Size; i++){
            auto devInfo = m_devInfoCollection->GetAt(i);
            auto location = devInfo->EnclosureLocation;

            if (location != nullptr){

                if (location->Panel == Windows::Devices::Enumeration::Panel::Front){
                    EnumedDeviceList2->Items->Append(devInfo->Name + "-Front");
                }
                else if (location->Panel == Windows::Devices::Enumeration::Panel::Back){
                    EnumedDeviceList2->Items->Append(devInfo->Name + "-Back");
                }
                else {
                    EnumedDeviceList2->Items->Append(devInfo->Name);
                }
            }else {
                EnumedDeviceList2->Items->Append(devInfo->Name);
            }
        }
        EnumedDeviceList2->SelectedIndex = 0;
    });

プレビュー機能を実装する

プレビューにメディアキャプチャクラスを使うので、MainPageの定義に以下の変数を追加します。

 Platform::Agile<Windows::Media::Capture::MediaCapture> m_mediaCaptureMgr;

プレビュー対象CaptureElementというものを使えばいいらしいのでXAMLに適当に定義します。 これとは別に適当にボタン貼っておいて、そのイベントハンドラをプレビューの起動トリガに使います。

        <Canvas x:Name="previewCanvas2" Width="320"  Height="240" Background="Gray" Margin="110,220,423,20">
            <CaptureElement x:Name="previewElement2" Width="320" Height="240" />
        </Canvas>

ボタンのイベントハンドラに以下のようなコードを入れます。 エラーチェックはしていません。

 m_mediaCaptureMgr = ref new Windows::Media::Capture::MediaCapture();

    auto settings = ref new Windows::Media::Capture::MediaCaptureInitializationSettings();
    auto chosenDevInfo = m_devInfoCollection->GetAt(EnumedDeviceList2->SelectedIndex);
    settings->VideoDeviceId = chosenDevInfo->Id;
    settings->StreamingCaptureMode = Capture::StreamingCaptureMode::AudioAndVideo;

    create_task(m_mediaCaptureMgr->InitializeAsync(settings))
    .then([this](task<void> initTask)
    {
        auto mediaCapture = m_mediaCaptureMgr.Get();
        previewCanvas2->Visibility = Windows::UI::Xaml::Visibility::Visible;
        previewElement2->Source = mediaCapture;
        create_task(mediaCapture->StartPreviewAsync());
    });

最後にPackage.appmanifestのWebカメラとマイクをONにしてビルド。エミュレータでやっても意味がないので実機でやります。

実行結果

  • 列挙されたデバイス : MN34150-back
  • プレビュー:表示された

というわけで、デバイス名とカメラの向き(front/back)まで調べてプレビュー表示できました。 もっといっぱいカメラを列挙できるかと思いましたが、1個しかなくて残念です。 (スペック表には4つの環境カメラや空間認識カメラなど載っていた気がしたのですが、今回のAPIでは取れないようです。)

どうしてback?

カメラのプロパティにbackというのがついてきます。これはカメラの向きの定義が「使用者のほうを向いているほうがfront」「反対方向がback」だからのようです。 ノートパソコンで画面と同じ向きに取り付けられているカメラはfrontで、画面と反対方向(メーカーのロゴとか入ってるほう)はbackだと考えると、まあ納得できます。 ちなみにUSBカメラはどっちでもないようです(そりゃ向きを自由に変えられるのだからfrontもbackもありませんよね。)

感想と今後の展望

やっとHoloLensでMR的なことをやる道具が揃い始めました。 MediaFoundationの近くまで進んだので、次回はMediaFoundationで画像処理を行って、その結果を取り出したいと思います。

HololensでMMDモデルをレンダリングしてみる[c++/UWP]

hololens DirectX11 C++

3Dモデルが出せたのでモチベーション上がり始めています。かたりぃなです。

環境 * HoloLens * Visual Studio 2015 * DirectX11 * C++/cx * UWP

とりあえずスクリーンショット f:id:Catalina1344:20170208220522p:plain やっぱりこうやって何かが出ると「アプリ作ってる」感でてきますね。 UWPでのDirectXの扱いに慣れていなかったので、詰まったポイントを整理していきます。 DirectX11そのものの扱いはほかにもっと詳しいサイトがありますし。

頂点シェーダが2種類存在している

vertex shaderですが、VisualStudio2015でHoloLens向けのテンプレートを生成すると、なぜかこれが2つ生成されます。

  • VertexShader.hlsl
  • VPRTVertexShader.hlsl

何者かというと、"VertexShader.hlsl"のほうがシミュレータで走らせるためのもののようです。"VPRTVertexShader.hlsl"はHoloLensで実行するものです。 しかしVPRTって何なのかよくわかっていません。あとで調べます。

なので、シミュレータで動くけど、実機で動かない(レンダリング周りの初期化でエラーになるとか)って時はこの2つのファイルの整合性を確認すると幸せになれそうです。

入力アセンブリの初期化で失敗する(CreateInputLayout)

先ほどのVertexShaderへの入力レイアウトの定義をもとにCreateInputLayoutするとシミュレータではうまくいくけど実機ではうまくいかないという罠にハマりました。 これもVertexShaderの入力が上記2種類で違いがあるために起きていました。 戻り値だけみても具体的に何が悪いのかわかりません(E_INVALIDARG)が、デバッグモードで起動していれ出力ウインドウに詳細を出してくれるので、よく読めばわかる問題でした。

実機でのレンダリング結果が壊れる(像がブレるなど)

HololensではDrawIndexedではなくDrawIndexedInstancedという関数を使います。 これは複数のインスタンスレンダリングするときに使うものです。

シェーダーでどちらのインスタンスを参照してレンダリングするかを決めています。(ここではCPUから渡された変換行列のどちらを参照するかを決めているだけ)

    int idx = input.instId % 2;
    pos = mul(pos, model);
    pos = mul(pos, viewProjection[idx]);
    output.pos = (min16float4)pos;

推測になりますが複数インスタンスとして左レンズと右レンズ用のインスタンスを作っていてそれを使い分けているのでしょう。

このあたりのインスタンスの参照先をうっかり書き換えてしまうと、像がブレる(というか、壊れて見える)ので要注意です。

あとは引数がやたら多いので、間違えないように注意です。

感想と今後の展望

とりあえず3Dモデルのレンダリングという基礎的な部分は作れるようになりました。モーションはシェーダの勉強と平行して実装していきたいところです。

あとHoloLensのSpatialMapping(空間認識)はちょっと独特(実際の物体よりも大きく空間を認識している)なので、アプリ側でもうちょっと改善できないか試してみたいところです。

最後にどうでもいいこと

HoloLensの空間認識は赤外線によるもの(Kinnectのような仕組み)だということなので調べてみました。 本当に赤外線出してるのかどうかスマホのカメラで見てみました。 アプリを起動していない状態では何も出ていないので、空間認識のサンプルアプリを起動した状態で撮影。 f:id:Catalina1344:20170207161543j:plain

写真のように光っています。これは一定のインターバルで点滅していました。やっぱり赤外線なんでしょうね。ほかのセンサがどう動いているかはこの写真ではわかりませんが。

Hololensで3Dモデルをレンダリングする[C++/UWP]

DirectX11 C++ Windows10 hololens

HoloLens実機で入出力回りをいじって色々と体感しようと試行錯誤です。かたりぃなです。 2017/02/06 追記:HoloLensからファイルピッカーを開くときの注意事項

この記事の意義

まずHoloLensを使ってプログラミングをしていく前にDirectXC++/cxを使う意義について整理したいと思います。

まず一番の目的は将来私自身が振り返った時、どこまで試したかを明確化しておいたほうが効率的だというのがあります。

次に現時点ではhololensでDirectXを扱った情報原が非常に少ないということです。 DirectX11でのWindowsプログラミングの情報は書籍でもWebでも充実していて、Windows上で3Dプログラミングをやりたいって人はそれで充分だと思います。 しかしC++/cxでのDirectX開発(UWP)となってくると情報が非常に少なくて、厳しいです。 というわけで、もし今後C++/cxでDirectX使ってHoloLensアプリを作りたいという人がいたとき、私の試行錯誤の記録が何かの参考になればと思っています。

では開発に入ります。 今回も開発ツールはVisual Studio 2015です。

2017RCが出ているので試してはみたいのですが、NuGetに2017対応のBoostが入ってないように見えたのでしばらく放置します。

言語はC++/cxです。

ジェスチャーを受け取る

Visual Studio2015でHoloLens用DirectXプロジェクトを生成すると、空間ジェスチャーの入力ハンドラが生成されます。 これは普通のUWPとかでは生成されないものです。実装はContent/SpatialInputHandler(.cpp/.h)です。 このクラス自身はジェスチャーをラッチするという単純な実装です。 具体的には

  • ジェスチャーを受け取って状態を保持する
  • 状態を取り出されたらクリアする

ラッチしたジェスチャーを取り出すのはアプリのMain(プロジェクト名にMain.cpp/Main.hサフィックスをつけたファイル)でやっていました。 ここのupdate関数(フレームごとに入力拾って立方体回してレンダリングする関数)でこんなのやってました。

#ifdef DRAW_SAMPLE_CONTENT
    // Check for new input state since the last frame.

    SpatialInteractionSourceState^ pointerState = m_spatialInputHandler->CheckForInput();
    if (pointerState != nullptr)
    {
        // When a Pressed gesture is detected, the sample hologram will be repositioned
        // two meters in front of the user.
        m_spinningCubeRenderer->PositionHologram(
            pointerState->TryGetPointerPose(currentCoordinateSystem)
            );
    }
#endif

単純に入力があったら、その視線の先に立方体を移動してあげるみたいです。 HoloLensエミュレータDirectXプロジェクトテンプレートをビルドしたものを放り込んで右クリックしたときに立方体がついてきていたのはこのルーチンがあったからなんですね。 今すぐにはジェスチャーによって何かするみたいな処理はやらない(というかやり方がよくわからない)ので、とりあえずここを間借りすることにしましょう。

UWPアプリでファイルを開く

3Dモデルのロード実験の方法としてはいくつか考えられます。

  • アプリにアセットとして組み込んでしまう
  • 都度ユーザーに選択してもらう

実験だけならどちらでもいい気がしますが、表示する3Dモデルの切り替えくらいできたほうが楽しいと思うので、後者でいきます。 前者も必要に迫られたらやると思います。 とりあえずユーザーがエアタップしたら先のコードが走ることはわかったので、書き換えます。

    SpatialInteractionSourceState^ pointerState = m_spatialInputHandler->CheckForInput();
    if (pointerState != nullptr)
    {
        auto openPicker = ref new FileOpenPicker();

        std::wstring cpath;
        openPicker->ViewMode = PickerViewMode::List;

        // ピクチャーライブラリーが起動時の位置
        // その他候補はPickerLocationIdを参照
        // http://msdn.microsoft.com/en-us/library/windows/apps/windows.storage.pickers.pickerlocationid
        openPicker->SuggestedStartLocation = PickerLocationId::PicturesLibrary;
        openPicker->FileTypeFilter->Append(".pmx");

        create_task(openPicker->PickSingleFileAsync()).then([&cpath](StorageFile^ file) {
            // 選択されたファイルに対してごにょごにょする
        });
    }

ちなみにこのコードを実行しようとすると、OneDrive入れてねと表示されるので、ストアから入れておきます。 理由は謎ですが今は目的のファイルさえ読めれば何でもいいです。 それにOneDrive経由のほうがテスト用のデータを投げ込むのラクになるので。 ファイルピッカーについての注意はFAQに載っていました。 同様の質問はStackOverFlowで出ていました(C#の場合)。 c# - Filepicker for Hololens: List available filepickers? - Stack Overflow

要約すると

  • HoloLensのファイルピッカーは「最初にインストールしたファイルストレージを扱うためのアプリ」を使って実現されている
  • ファイルピッカーとして使うアプリの切り替えはできない
  • つまり後から別の使いやすいエクスプローラを入れても、それを使うように切り替えたりはできない
  • OneDriveお勧め

だそうです。 今はこういう仕様とのことです。まあ開発者版だから仕方ないですね。

ファイルから読む

UWPでのファイル読み込みはMSが公開しているサンプルから適当なものを拝借しました。

https://github.com/Microsoft/Windows-universal-samples.git

さっきのファイルピッカーで選択されたファイルを全部バッファに読み込むまでのコードです。

     create_task(openPicker->PickSingleFileAsync()).then([this](StorageFile^ file) {
            this->path = file->Path;
            return FileIO::ReadBufferAsync(file);
        }).then([this](task<IBuffer^> task) {
            auto buffer = task.get();
            auto reader = DataReader::FromBuffer(buffer);
            auto length = buffer->Length;
            auto loaded_buffer = reader->ReadBuffer(length);
            parse_(this->path->Data(), loaded_buffer);
        });

ファイルのパスをthisに保持しているのはデバッグ用です。 また今回は魅力的なモデルが多数公開されているMMDを使うのですが、どうもテクスチャのイメージファイルのパスが相対パスで記述されているものもあるみたいなので、いったんここでとっておくようにしています。 ちなみにここで拾ったpathをC/C++の標準関数で開けないかと試してみましたが、「アクセス権がない」といわれて開かせてくれません。 OneDrive経由のファイルだからなのか、このあたりはよくわかりません。

boostをインストール

Nuget経由でboostをインストールします。わざわざビルドしなくて済むのいいですね。 boostは簡易な機能であればヘッダだけで使えるのですが、今後のことも考えてライブラリまとめて取り込んでおきます。 スクリーンショットで選択されているものをインストールすれば依存関係の解決でヘッダもついてきます。 boostを使う理由は後述。 f:id:Catalina1344:20170205130208p:plain

どうしてboost?

ライブラリのインターフェースとして「ストリームやファイルから読み込んで処理する」みたいなのが多々あります。 今回使おうとしているパーサなどもこういったインターフェースです。 ファイルの読み取りはUWPの機能を使って実装したので、あとはインターフェースの橋渡しをしてあげれば話は簡単になります。 (ライブラリ側には手を入れなくていいし、ファイルアクセスはUWPの枠組みで行える) この橋渡しをするために、basic_ivectorstreamというクラスを使うことにしました。 このクラスを使えばc++コンテナをistreamとして扱えるようになります。便利ですね。 というわけで、プログラムの構造はこうなりました。

  1. UWPのランタイムで提供される機能を使ってファイルの内容を読み取る
  2. UWPのバッファのアドレスをCOM経由で取り出す
  3. std::vectorとしてバッファを準備して、そこにデータをコピー
  4. Boost::basic_ivectorstreamでstd::vectorをラップする
  5. ラップしたデータをistreamとしてライブラリに渡す(ここではMMDパーサ)

すごく回りくどいことになりましたが、いったん何かを作りたいのでtodoコメントだけ入れておきます。 ちなみにComPtrを使うにはwrl/client.hが必要です。

 // IBufferオブジェクトからC++標準のistreamを作りたい
    Object^ obj = buffer;
    Microsoft::WRL::ComPtr<IInspectable> insp(reinterpret_cast<IInspectable*>(obj));
    Microsoft::WRL::ComPtr<IBufferByteAccess> bufferByteAccess;
    DX::ThrowIfFailed(insp.As(&bufferByteAccess));
    byte* pmx_raw_data_ptr = nullptr;
    DX::ThrowIfFailed(bufferByteAccess->Buffer(&pmx_raw_data_ptr));

    // COMインターフェイス経由で得たアドレスを使ってvectorを生成したい
    // いい方法が思いつかないのでコピーで済ませる : todo改善ポイント
    std::vector<char>    pmx_raw_data;
    pmx_raw_data.resize(buffer->Length);
    CopyMemory(&pmx_raw_data[0], pmx_raw_data_ptr, buffer->Length);

    boost::interprocess::basic_ivectorstream<std::vector<char> >  input_vector_stream(pmx_raw_data);

これで、以降はinput_vector_streamをistreamとして読めるようになります。 中身にはもちろんファイルから読み込んだデータが入っているので、istreamを扱うパーサライブラリさんとboostさんが連携して適切に処理してくれます。 改善ポイントは多々ありますが、それは追々やっていきます。 mmdのファイル中にテクスチャ画像ファイルへの参照パスが含まれていますが、今はおいておきます。

DirectXを叩く準備をする

やっと最低限の下地が整いました。 DirectX11に対してパースした3Dモデルのデータを投げ込んでいきます。 VisualStudioテンプレートで生成したコードではSpinningCubeRendererクラスがDirectXでのレンダリングを担当しています。 #if DRAW_SAMPLE_CONTENTで切り替えられているので、レンダリングするコンテンツにあわせた最適なクラスを作れという意思表示でしょう。 しかし今回は「どういった感じなのか」という感触をつかみつつ、何かをレンダリングするのが目的なので、このクラスを直接変更していくことにします。

DirectXで3Dダリングするために必要なもの

初期化周りはややこしいので放置します。幸いなことに別クラスで面倒見てくれていますし。 CreateDeviceDependentResourcesに記述されているものを順に整理します。

  1. プログラマブルシェーダ
  2. シェーダーへの入力レイアウト
  3. 3Dモデルの姿勢を表す変換行列
  4. 立方体の頂点情報(xyz座標,rgbカラー)
  5. 頂点情報からポリゴンを作るためのインデックス指標

mmdファイルをパースして取り出した情報を設定する対象は4と5です。 生ポリゴンをレンダリングするだけならこれで充分です。

できればテクスチャ貼りたいところですが、VisualStudioのDirectXテンプレートではシェーダーが受け取る頂点フォーマットにテクスチャ座標が含まれていないうえ、そもそもテクスチャサンプラとかの初期化も入っていないので、大工事になってしまいます。

年度末の道路工事をやるとしても部分的にやっていったほうが全面通行止め箇所も少なくて通りやすい=安心できるので、テクスチャはシェーダー含めて今後やっていきたいと思います。

ファイルから頂点とインデックスを取り出す

作業を細分化して話は簡単になりました。3DオブジェクトのジオメトリのみDirectXに与えればいいわけですね。 やってみましょう。

まずファイルから読み込んだ頂点とインデックスを適当にクラスメンバに保存しておきます。変数名がなんかアレですがMSのサンプルの名前付け規則に合わせました。

型ですがDirectXテンプレートのデフォルトの頂点シェーダに合わせるようにしています。 頂点型がfloatなのは良いとしても、インデックス型がunsinged shortって不安ですね。 しかし幸いなことに手元の3Dモデルではindexの上限が65535(=0xFFFF)なので助かりました。(古いフォーマットからコンバートされたものなのだろうか。。。)

// SpinningCubeRenderer.h
namespace HolographicApp1
{
    // This sample renderer instantiates a basic rendering pipeline.
    class SpinningCubeRenderer
    {
    private:
        std::vector<float>           m_vertics;
        std::vector<unsigned short> m_indics;
// SpinningCubeRenderer.cpp
    const int elements_of_vertex = 6;
    auto vertex_num = reader.total_vertex_num();
    m_vertics.resize(vertex_num * elements_of_vertex);

    float scale = 0.01f;
    for (size_t i = 0; i < reader.total_vertex_num(); i++) {
        auto r = reader.get_vertex(i);
        m_vertics[i*elements_of_vertex + 0] = r.position[0] * scale;
        m_vertics[i*elements_of_vertex + 1] = r.position[1] * scale;
        m_vertics[i*elements_of_vertex + 2] = r.position[2] * scale;

        m_vertics[i*elements_of_vertex + 3] = 1.0f;
        m_vertics[i*elements_of_vertex + 4] = 1.0f;
        m_vertics[i*elements_of_vertex + 5] = 1.0f;
    }

まずscaleについてですが、そのままのスケールで表示してしまうと画面(実機では視界)からはみ出す巨人が出てきてしまいました。 xyz全体にとりあえず適当なスケールかけておきます。

次にフォーマットです。 このデータ列の塊をDirectX経由でGPUに転送するわけですから、頂点シェーダーの読み取るフォーマットに合わせておかないと大変です。 VisualStudioが生成したDirectXのテンプレートに含まれているシェーダー入力では頂点フォーマットはxyz,rgbの6要素からなります。 xyzにファイルから取り出した頂点の座標を入れていきます。カラー情報今は無視したいので1.0でも設定しておきます。 0.0にしてしまうと何も表示されないことになってしまいます。 これはHoloLensが光を投影するという原理であるための制約ですね。たぶん。(黒い光は出せませんよね。。。) PC向けUWPアプリなら背景色を黒以外に設定しておけば0.0でも見えるとは思います。

次にインデックス

 auto index_num = reader.total_indics_num();
    m_indics.resize(index_num*3);

    auto face_num = reader.total_face_num();
    for (size_t i = 0;i < face_num; i++) {
        auto face = reader.get_face(i);
        auto* face_buf = &m_indics[i * 3];
        face_buf[0] = face[0];
        face_buf[1] = face[1];
        face_buf[2] = face[2];
    }

今回使用したファイルパーサのインデックスの出力は「3つのインデックスで1つの三角形ポリゴンを表現する(TRIANGLE_LIST)」ので、こうなります。 どちらの例も本来なら適当な構造体を定義してそれの配列とするべきでしょうけれど、まあこれで一旦やってみましょう。 構造体作るとアライメント考えないといけませんし。

つくった頂点とインデックス情報をDirectXのバッファに書き込みます。いわゆるGPUのVRAMに転送ですね。 まず頂点バッファ

     D3D11_SUBRESOURCE_DATA vertexBufferData = { 0 };
        vertexBufferData.pSysMem = &this->m_vertics[0];
        vertexBufferData.SysMemPitch = 0;
        vertexBufferData.SysMemSlicePitch = 0;
        const CD3D11_BUFFER_DESC vertexBufferDesc(sizeof(float) * this->m_vertics.size(), D3D11_BIND_VERTEX_BUFFER);
        DX::ThrowIfFailed(
            m_deviceResources->GetD3DDevice()->CreateBuffer(
                &vertexBufferDesc,
                &vertexBufferData,
                &m_vertexBuffer
            )
        );

次にインデックスバッファ

     m_indexCount = m_indics.size();

        D3D11_SUBRESOURCE_DATA indexBufferData = { 0 };
        indexBufferData.pSysMem = &m_indics[0];
        indexBufferData.SysMemPitch = 0;
        indexBufferData.SysMemSlicePitch = 0;
        CD3D11_BUFFER_DESC indexBufferDesc(sizeof(unsigned short) * m_indics.size(), D3D11_BIND_INDEX_BUFFER);
        DX::ThrowIfFailed(
            m_deviceResources->GetD3DDevice()->CreateBuffer(
                &indexBufferDesc,
                &indexBufferData,
                &m_indexBuffer
            )
        );

m_indexCountはクラスのメンバ変数として最初から定義されていたのですが、実際のレンダリング時にこれを使っていました。

    // Draw the objects.
    context->DrawIndexedInstanced(
        m_indexCount,   // Index count per instance.
        2,              // Instance count.
        0,              // Start index location.
        0,              // Base vertex location.
        0               // Start instance location.
        );

というわけでこれで生ポリゴンを出す準備は整いました。

動かしてみる

最低限のポリゴンレンダリングのコードを書いたので動かしてみました。 実機で動かしても同様の結果が得られました。

実機で動画とってアップしようかと思いましたが、机の上どころか部屋が散らかりすぎていてアップロードするのはキツイです。 エミュレータスクリーンショットです。

f:id:Catalina1344:20170205185659p:plain

シルエットはきちんと表示されていますね。まずはOKですね。

課題

実機で動かしてみて、とりあえず机の上に置いてみました。フィギュアみたいです。 VRもそうですがとりあえずお約束としてまずは覗き込みますよね。みえ。 見えたけど、内側から見るとスカートが見えなくなってしまいました。どうやらこのモデルは外側からしかポリゴン貼ってないようです。 AR/VRでアプリを作るときはカリングモードの設定とモデルの作り方に要注意ですね。

また、Hololensのデモやアプリの実装を見ていると、現状のソフトウェアではまだまだ機能が足りないなと思う部分があります。 たとえば表示されているホログラムと実世界のつながり。

Hololensは現実世界の空間マッピングを行ってマッピング情報をもとに適切な位置にホログラムを表示できるのが基本です。 いろいろ触ってみて感じたのは「表示されているホログラムを掴みたい」です。 ホログラムを掴むためには表示されているホログラムの情報を単純に空間マッピングに書き戻せばできそうですが、それってHololensのマシンスペックで解決できるレベルなのかどうか少々不安です。 「掴む」ことができないならマーカー型ARみたくマーカー置けば似たようなことはできそうな気はします。

今後の展望

週末2日間でそれっぽいところまでできました。 あとはテクスチャを張ってモーション再生してあげれば楽しそうです。(やっとUnity組に追いつける!)

いくつか気になる点があるので次はこのあたりから手を付ける予定です。 1. MMDパーサがテクスチャ読み込もうとしてエラー吐いている 2. モデルの配置位置が固定になってしまう

まず1。今の仕組みだとエラー吐いて当然だと思います。OneDriveのファイルをダウンロードしたパスからテクスチャを読もうとするので、ファイルのダウンロードが終わっている保障はどこにもありません。というかダウンロード命令出していません。 ファイル操作まわりをもっとラクに扱えるように整理したいですね。

次に2。今回の実装でジェスチャの機能を殺している(エアタップされたら読み込むモデルを選択する)ので、このあたりも使い方を調べて適切なUIを作りたいですね。

Hololensが届いた

DirectX11 C++ UWP hololens

やっとhololensが届いたので開封の儀式です。かたりぃなです。

 

開封

f:id:Catalina1344:20170131202531j:plain

 とりあえず開封した直後の写真です。この写真中央部の奥にマニュアルが入っていました。

どうせマニュアル全部英語だろと決めつけて冊子を半分読んだところで、残り半分が日本語マニュアルという事実。ショックです。

 

初期化

マニュアルに従って装着して電源オン。

音声ガイドに従って初期設定していきます。音声ガイドは英語です。KIAIで解読しましょう。画面表示内容みれば何言ってるか推測はつきます。

まずキャリブレーションから。表示されるカーソル位置に指をあわせてキャリブレーションしていきます。

指紋認証か何かかと勝手に深読みしてしまいましたが、そんなことはなかった。

 

操作方法ガイドを一通り試す

キャリブレーションが終わると操作方法のガイダンスがはじまります。

指をすぼめて上に向けた状態から開くのがブルーム。

人差し指を立てた状態から倒すエアタップ。

長押しするとスクロールバーとかつまめるよみたいなのも。

クリッカーの設定もここでやります。クリッカー側のペアリングボタンは細いものがないと押せないので、事前に準備しておいたほうがいいです。とりあえず手元に転がってたボールペンの先端で押しました。

 

アカウント設定

ガイダンスを終えるとアカウント設定です。

ここから先はHololens Emulaterと同じ手順です。

ちなみにこの時点ではBlueToothキーボードは未設定(設定するタイミングが無い)なので、クリッカーと視線で頑張って入力します。(使えるキーボードがまだ手元にないから、どっちみち使えないのですが。)hololensの視線入力はこういう細かい操作には向きませんね。慣れの問題かもしれませんが。

 

とりあえず起動

アカウント設定が終わばあとは好きな事ができます。

3Dオブジェクトを好きな位置に配置したり、適当なゲームを試したりと。

部屋の壁に穴が開いて、虫が湧いてくるのを退治するゲームを試してみましたが結構面白いです。

ただ、視野角が狭いので視線だけで対象を追おうとするとホログラフィックが消失してしまいます。視野角については今後の改善に期待ですね。まあ暫く使っていけば「こんなもんだろ」と慣れてくると思いますが。

 

アプリを書き込む

早速アプリを書き込みたいところですが、まだ何も作っていません。

でもせっかくだから何か書き込みたいですよね。というわけでDirectXのUWP(holographic)のサンプルコードを投入しましょう。

ビルドしてターゲットデバイスとUSB接続。デバッグ開始を押すと始まります。

ただし初めての書き込み時はPINコードの設定をしろと言われるのでこのあたりを参考に設定します。設定のセキュリティのとこにあります。

表示されるコードをVisualStudioに入力すればOKです。

初めてのHoloLens開発(2D/Holographicアプリ)[環境準備~実行、HoloToolkit-Unity活用] - Build Insider

 

とりあず動きました。スクリーンショットの撮り方がわからないのでまた別の機会に。

 

今後の展望

もうすでに何かやってる人いないかなと思って色々と調べましたが、UnityとVuforiaの人が多かった印象です。手軽に作って試すにはよさそうです。

イノベーションを起こすならフットワークの軽さは重要だと思います。

ただ、そうして先端を走り続けるには限界があると思っている(どこかでフレームワークの限界が来る)ので、マイペースでいきます。下から攻めたほうが有利というか、人の上を行くなら下から行けみたいな格言もありますし。

本質的な部分を押さえておけばHololensが普及しなくても応用が利きます。まあ結局ライブラリ叩くんですけどね。

というわけで今年一年くらいのペースでやりたいことを技術要素に落とし込んだものをリストアップしてみました。

  1. ソフトウェアで映像入力を受ける(Microsoft MediaFoundation)
  2. 映像を処理して、ある共通する特徴を持った物体(カードとか)の特定物体認識を行う(OpenCV-imgproc etc.)
  3. 特定物体認識で検出した物体をトラッキングする(OpenCV-opticalflow etc.)
  4. 検出した物体の詳細を(カードの絵柄とか文字列とかを使って)分類する(chainer - machine learning)
  5. 検出結果をもとにWebから追加情報を拾ってくる(UWP-HTTPClient)
  6. 3Dオブジェクトのレンダリングを行う(Microsoft DirectX11)

なんとなくですが2と4が一番ネックになりそうな気がしています。

だいたい以前やったことのブラッシュアップなので、結構なんとかなる気がしていますし、ブラッシュアップの段階でHololensの機能を借りれば色々と良くなりそうです。

例えば

ARの原理実験 - catalinaの備忘録

Chainerでcifar-10画像分類を試してみる - catalinaの備忘録

Windowsストアアプリを作ってみる - catalinaの備忘録

openGLでMMDモデルを表示する - catalinaの備忘録

だけでも最低限のプロトタイプくらいはできそうです。

あとは機械学習をどう活用するか考えながら色々実験していきたいですね。

Hololensのスペック的にDNNを載せるのはきつそうなので、そこも工夫が必要そうかなと思います。

 

あ、DirectX版のレンダリングの記事書いていないのに気づきました。

UWP版を作るときに整理します。とりあえず今回は写真だけ。適当なパーサ落としてきてDirectX11でレンダリングしてみたところです。

色合いがおかしいのは適当に書いたHLSLシェーダーのせいです。確かMMD本家はトゥーンですよね。。。

f:id:Catalina1344:20170129221225p:plain

UWP版への移植はこれからですが、とりあえず表示まで行ってるので、あとはなんとかなる気がしています(というかMMDはライセンス面倒だから他のフォーマットがいいな。。。)。

では今回はこれくらいで。