ユニティちゃんを超ARしたときの話

(※こちらは過去の記事を移転したものです)

前回、UnityでリアルタイムIBLしてみたというエントリを書かせて頂きましたが、今回その応用バージョンで、IBL+AR=超ARしてみたのでその時のメモやら実装方法を残しておきます。

動画は以下のリンクにうぷしてますので、先に見てもらうと、「あら、こんな感じの事できるのねー」と雰囲気理解してもらえると思います。

微妙にマーカーズレてたり、解像度低かったりしたので今度取り直してYoutubeに再アップします。。

##今回やった事
前回記事をベースに、実装面で進化しているのは大きくは以下の点です。

  • IBLで使うCubemapTextureを静止画から動画に対応
  • WebCamからの映像をIBLに反映
  • AR対応

現実とのインタラクションをより感じさせるために今回ARを使っていますが、任意の動画環境下で動きます。
Webcamだと解像度とかに限界を感じたので、インタラクションを無視できるなら、それっぽい動画を撮影して後でそれを流した方が雰囲気出るのかも知れないです。

ちなみに技術的に新規性のある事とかアイデアのある事は一切やっておらず、CG屋さんが使いそうなテクニックをそのまんまUnity上で行ってARに載せているだけです。

Youtubeでは本デモのようなImage-BasedなARは少なからずあって、海外の人がいくつか上げてるんですが、日本ではIBL+ARっていうのはあんまり見ないですね。

今回のデモではしっかりアンビエントとか焼いたり、シャドウを付けたり、細かい事しているので、演出面ではそこそこ雰囲気を出せたのではと思っています。

なによりユニティちゃんのアセットのクオリティが高い!

あと、先に言っておくと、ARについては特に特別な事はやっておらず、Unity ARでググったページを見ながらSDKインポートしたくらいしかしていません。

なので、ここからはほぼほぼレンダリングの話になります。

環境とか

今回の動作環境は以下の通りです。

  • Mac Book Rro Retina 15-inch 2013
  • OS X 10.9.5
  • Unity Pro 4.5.4f1

今回は禁断のUnity proを使用しています!ドヤァ!

ただこれは一ヶ月限定のイベントライセンスなので、来月には切れてノーマル無課金厨に戻ります。

今回はたまたま「ユニティちゃんディレクター杯」というコンテストをUnityさんが主催されていましたので、参加登録してイベントライセンスを付与して頂きました。

そして、今回の実装ではProでしかできない事をやっちゃってます。

詳しくは”実装方法”のところで述べますが、シーンをCubemapに焼くAPIはPro限定っぽいです。

ただ、今思えば別にこのAPI使わなくても実装できたんじゃね?と思っているので、今後暇だったらFree対応するかもしれません。

実装方法

1. アセットを準備する

まずはUnityちゃんのきれいなテクスチャを作りましょう。
ARにした時に、現実となるべくなじませるためには、アンビエントやシーンに非依存の環境光を焼く作業はほぼ必須だと思っています。

前回の記事と同じようにBlenderのCyclesでユニティちゃんをレンダリングしてテクスチャを刷新して下さい。

Cyclesでレンダリングするとたぶんこんな感じになります。

cycles

ユニティちゃんはミクと違って法線マップを持っていたり、そもそもテクスチャにアンビエントっぽい陰が乗っていたりするので焼かなくてもそれなりにリアルになるかもしれません。

ユニティちゃんは出来るコです。

テクスチャを焼きおえたらUnity上でテクスチャを差し替えて、Luxのマテリアルを各パーツに設定してください。

Luxについては前回記事を参照下さい。

自分は肌系はLux/Bumped Diffuse、洋服系はLux/Bumped Speculaer、髪はLux/Human/Hairを設定しています。

ここまでいくとこのくらいのクオリティまで出せます。

ibl

2. IBLを動画に対応させる

前回の実装ではIBLに使うDiffuse CubemapとSpecular Cubemapは事前に用意した静止画だったのですが、今回はこれらを動的に作成することで任意の動画をCubemapとして使えるようにします。

まず毎フレームIBLのCubemapを変更する方法ですが、自分はSetupLuxクラスを継承して書き換えて実装しました。

書き換えたCustomSetupLuxクラスはgistにのせときますので細かいところはこれを読んでください。

(また、継承もとのSetupLux.csはこちらにあります。)

CustomSetupLuxの中でCubemapを書き換えている部分は以下の部分。

spec = new Cubemap(cubemapSize, TextureFormat.ARGB32, true);
diff = new Cubemap(cubemapSize, TextureFormat.ARGB32, true);

Shader.SetGlobalTexture("_SpecCubeIBL", spec);
Shader.SetGlobalTexture("_DiffCubeIBL", diff);

オリジナルのLuxのSetGlobalTexture後に、こちらで一回SetGlobalTextureしておけば、あとはspec, diffの中身を書き換えていくだけで勝手にLuxがよしなに処理してくれます。

また、Cubemap型はイコールオペレーションではコピーされない点に注意です。
以下のようにColor[] colorをセットします。

diff.SetPixels(color, face);
diff.Apply();

あとはこのdiffとspecの更新処理をLastUpdate()内で呼んであげれば毎フレームShaderのテクスチャが更新されるようになると思います。

また、SetupLux(というかMonoBehaviour)の関数をオーバーライドする際の注意点ですが、継承元の関数の指定子をpublic virtualに変更してからオーバーライドして下さい。じゃないと継承元が呼ばれません。

// SetupLux側
public virtual void Update () {
    hogehoge();
}

// CustomSetupLux側
public override void Update () {
    base.Update ();
    fugafuga();
}

3. WebCamからの映像をIBLに反映させる

diffとspecがそれぞれリアルタイム更新されるようになったので、これに値を入れていきます。

IBLを実現するためには、ざっくり言うとユニティちゃん視点から周囲を映したテクスチャを作り、そのテクスチャのピクセル値をdiffやらspecやらに詰めていけば良いわけです。

そのためにはCameraクラスのRenderToCubemap()を使います。
使い方は以下の要領で、faceで面を指定しながらコールします。

cam.transform.position = cameraPos;
cam.RenderToCubemap ( spec, face);

CustomSetupLuxクラスではUpdate()内でユニティちゃんの移動に合わせてcameraPosを更新し、それを上記のようにcam(ユニティちゃんサイドからみた仮想視点)にセットしてRenderToCubemapしています。

このような修正をすればユニティちゃんから見たシーンの映像がspecに格納されるようになります。

ただ、これだと周囲になんにも置かない限り真っ青なシーンがレンダリングされるだけだと思うので、シーンに半球をおいてそこにwebcamの映像を投影し、それをcamに撮影させます。(なんてワークアラウンド)

cam

半球モデルは探しても無かったので自作しました。

semi_sphare.objは映像投影用に法線方向を反転させて、半球内に描画されるようにモデリングしてあります。
UVも透視投影で良い感じにマップしてあります。

ちなみにBlenderで作ったのですが、OBJ以外Unityでうまく読んでくれなかったのでモデルをエクスポートする時はフォーマットに注意して下さい。

この半球をシーンのどっかにおいて、その中をcamが動くように調節します。
半球を二つ置いて閉じちゃう方が映像にムラが無くて良い感じです。

この半球に対してWebCamの画像を映す方法ですが、以下のスクリプトを半球に設定すればOKです。

中身はわりとシンプルなので、コード見てもらえば分かると思います。

これでWebcam映像がspecに映るようになると思います!

4. いい感じにテクスチャをぼかす

環境光がキューブマップに焼けたのは良いのですが、specular用のCubemapとdiffuse用のCubemapは全然求められるものが違います。

  • specular map

spec_map

  • diffuse map

diff_map

上図はちゃんと計算されたspecularテクスチャとdiffuseテクスチャの違いなのですが、diffuse用はむちゃくちゃぼけてるのが分かると思います。

diffuseはオブジェクト表面でいろんな方向に拡散した光をシミュレートしたマップなので明るくぼけた印象のテクスチャになってます。

これを再現するために、今回は超単純ですが線形ブラーを実装しました。

しかも、GPUとか使わずに、めんどくさいので思いっきりC#で書いています。

参考にしたのはこちらのCommunityで議論されているブラーです。

ただこの実装だとキューブマップの境界でアーティファクトが発生してしまうので、実際には隣り合うマップの重なる部分もぼかしてスムーズにブラーがかかるようにしています。

CustomSetupLuxクラスのFastBlur()が実装したものです。

若干怪しい挙動しますが、まぁ許容範囲内です。。

そして、Fastと言っていますが、ものすごく遅いです。

遅すぎて毎フレやるとユニティちゃん動かないレベルです。

ですので実際にブラー処理をするのは数フレに一回程度にしましょう。
ついでにcubemapを書き直す事自体もそう頻繁にやる必要ないので、数フレに一回だけ書くようにします。

また、SpecularマップもDiffuseマップも高い解像度はまったく必要有りません。
雰囲気だけ合っていればよいし、映り込む領域も小さいので、Cubemapの解像度は16x16とかでOKです。

今回のデモで言えば、specは16x16で作って、diffuseはそれに半径8のブラーをかけてる感じです。

また、やってみると分かるのですが、こうして作ったdiffuseはとても暗いです。
本来はもっと光が漏れだしてほしい部分が明るくなってくれません。

かといって一律でoffsetを履かせると255を超えてしまう部分もあるので、サチらないように明るさを調整する必要があります。
つまりガンマ補正をかけます。

ガンマ補正は255諧調でいうと以下の感じで実装できます。たぶん。。

Color[] GammaCorrection(Color[] input, int width, int height, float gamma) {
    Color[] output = new Color[width * height];
    for (int w = 0; w < width; ++w) {
        for (int h = 0; h < height; ++h) {
            output[width * w + h].r = 255.0f * Mathf.Pow(1.0f / 255.0f * input[width * w + h].r, 1.0f / gamma);
            output[width * w + h].g = 255.0f * Mathf.Pow(1.0f / 255.0f * input[width * w + h].g, 1.0f / gamma);
            output[width * w + h].b = 255.0f * Mathf.Pow(1.0f / 255.0f * input[width * w + h].b, 1.0f / gamma);
            output[width * w + h].a = 255.0f;
        }
    }
    return output;
}

これで明るさを調整できるので、程よい値を探しながらレンダリングしてみて下さい。

4. ARしてみる

AR自体は特別な事はしていません。
以下のサイトを参考にして設定しました。

ちなみに、どこのサイトでもTargetDimensionは100で設定していますが、たぶんこれはデマで、Unity上のアセットの大きさを1を基準にしている場合は普通に1で良いです。

また、デモではキューブ形状のマーカーを置いていますが、実は前面のプレーンしか使っていません。

キューブでもやりましたが、なんか方向が逆に定まらず、一面だけの方が安定しました。

なので、キューブである必要は全くないのです。

あと、本デモではシャドウについてはシャドウの部分だけしっかりマスクして、あたかも床に影が出ているかのように演出しています。

床オブジェクトやマーカーキューブオブジェクトに以下のShaderをマテリアルを設定すれば、シャドウだけ表示する事が出来ますので参考にしてみてください。

結果と考察

setup

こんな感じでライトとステージを用意してライトをひたすら振りながらwebCamでユニティちゃんを追ってました。

res1
res2
res0

動画を見てもらうと分かりますが、ライトが変化するとそれに合わせてユニティちゃんもライティングされるのが分かります。

一応ユニティちゃんのポジションを追っているので、ライトに近づけば近づくほど色がつく(はず)

ちなみにマーカーとシャドウが微妙にズレているのは完全な設定ミスです。

本当はぴったり合いますが、血反吐を吐いていたので、このとき気づきませんでした。

思ったよりシーンに馴染んだのと、シーン変えてもどこでも割といい感じにIBLできたので、個人的には満足いってます。

あとは、シャドウが結構不自然なのでそれは直せたら直したいです。

平行光源を手動で一点置いちゃってる(主にシャドウ用)のでそれもできれば自動推定or外したいです。

でもシャドウをリアルにやるにはどうすれば良いのか。。

最後に

キャプチャするのに毎フレーム書き出していたんですが、それが激重で、そのまったりとした動きに合わせてカメラを動かすという作業は本当に骨が折れました。
なんか手軽にリアルタイムキャプチャできる方法があればご教授いただきたいこのごろです。

末筆になりますが、今回のデモは多くの方にTwitterやFacebook上で取り上げて頂き、制作者としてはとても嬉しく思っています。
こんなすばらしい記事になったりしてすごくモチベーションが上がりました。

アドバイスや機材提供など、ご協力いただいた方々にもあらためて感謝したいと思います。
ありがとうございました。

##ライセンス表記

ユニティちゃんライセンス

このコンテンツは、『ユニティちゃんライセンス』で提供されています