(アーティストさんへ)LDRなブルームの注意点、そしてHDRとLDRについて

f:id:hirasho0:20191003165340p:plain

こんにちは。技術部平山です。

以前LDR(Low Dynamic Range)なブルームエフェクトについて書きましたが、 今回はLDRでブルームをやる時に起こる問題点について、アーティストさん向けに書いてみます。

結論から言うと、「後から足すのはそう簡単じゃない」という事になります。 実際、最近とある製品で「入れたい」という話になったのですが、 保留という結論になってしまいました。

LDRで実装したものには結構な制約があり、 「後からチョロっと入れておしまい」にはしにくい事情があるのです。

0から255しかない

ゲームでは毎フレーム全画面の絵を描き直すわけですが、 その時に書き込むキャンバスには、大きく分けて二種類あります。

  • LDR(Low Dynamic Range。ローダイナミックレンジ)
  • HDR(High Dynamic Range。ハイダイナミックレンジ)

LDRと言えば普通、0から1の範囲の数字を、256段階で書き込みます。 1画素の容量はRGBとアルファそれぞれに1バイトで、4バイトです。

大抵HDRと言う時には、0から65000の範囲の数字を、65000段階で書き込みます。 数字が小さい所では段階が細かくなるように工夫されており、 0から1の間はだいたい2000段階くらいになります。 LDRでは256段階しかないですから、8倍細かいわけです。 その上、10とか100とか1000といった大きな値も書き込めるので、 LDRとは表現力が全く違ってきます。 そのかわり、1画素の容量は倍の8バイトです。

ダイナミックレンジというのは「表現の幅」のことでして、 LDRでは0から1しかないものが、HDRでは0から65000まで広がるわけですから、 HDRの方がいいに決まっているわけです。

しかし、残念ながら、容量が大きいものは遅いのです。 また、古い機械ではそもそもHDRが使えません。 とはいえ、本来、ブルームエフェクトはHDRなキャンバスが前提の技術です。 それをLDRでやるのは基本的に無理をしている、ということを、 知っていただきたいと思います。

では、その無理の詳細に踏み込んでいきましょう。

キャラの光沢をまぶしくしたい

とりあえず冒頭の画面を見てください。 アレなキャラの頭がギラリと光っていますね。 ブルームエフェクトによって、ハイライトがポリゴン境界をはみ出して光っています。 アニメ的な表現を想定していて、背景はQuadにテクスチャをベタ貼りして 照明計算はしていません。 しかし、この絵を作るには若干の手間をかける必要があるのです。

まず、何も考えずにやってみましょう。 背景とキャラを普通に配置します。

f:id:hirasho0:20191003165400p:plain

キャラはメタリックなマテリアルにして、ハイライト以外は暗めにしておきました。

さあ、ブルームを有効化しましょう。

f:id:hirasho0:20191003165348p:plain

白飛びしすぎですね。 入れた設定は、

  • RGBそれぞれが0以上のところを
  • 1倍の強度でぼかして足す

というものです。 黒い所はほとんど広がりませんが、白い所は白が広がり、 赤い所は赤が広がるので、まあこうなります。 すごく明るい所だけを広げたいのであって、 大して明るくない所は広げなくて良いのですから、 これは設定が悪いですね。 では設定を変えてみましょう。

f:id:hirasho0:20191003165350p:plain

マシにはなりましたが、背景は光らなくていいですよね。 この設定は、

  • RGBそれぞれが0.5以上のところだけを取り出してぼかす

というものですが、0.5以上という条件だと 背景がこんなに光ってしまいます。光るのはキャラの頭だけでいいのです。 もっと数字を上げてみましょう。

f:id:hirasho0:20191003165354p:plain

0.9以上の所だけを取り出すことにしましたが、やっぱり背景光ってますね。

まあ当然のことで、背景の画像を見ていただければわかるように、 真っ赤な所や、すごく緑な所があるわけです。 真っ赤な所はRが1くらいあるわけで、 0.9なんて余裕で超えてしまいます。

え、どうすればいいの?ということになるわけです。

HDRならこんな問題は起こらない

問題は「赤かったり緑だったりする所」、つまり背景と、 「すごく光ってる所」、つまりキャラのハイライトの区別がつかないことです。 白は1でして、太陽がガッツリ反射してすごく明るい所も同じく1です。 後者だけを取り出す、ということはそもそもできません。

でも、もしキャンバスがHDRであれば、こんな問題はありません。 65000まで書けるからです。キャラのハイライトは100とか1000とかを書き込み、 背景はたかだか1くらいまでしか書き込まない、 ということになれば、区別は容易につきます。 すごく明るい所だけ取り出してぼかすのは簡単です。

問題は、「1までしか入らない」ということにあるのです。 LDRでブルームをやるのが無茶、ということの意味がおわかりかと思います。

ではどうするか?

手はいくつかあるのですが、一番既存処理に影響を与えないでやれる手は、 「LDRなキャンバスに1より大きいものが入るようにする」です。

え、と思うかもしれません。「LDRなキャンバスは0から1しか入らない」 ってさっき言いましたからね。

しかし、解釈を変える、という手があります。 「1が入っていたらそれは10のことだ」と後で解釈しなおせばいいのです。 つまり、最初に描画する時に1/10の値を書き込みます。 背景の白は1を1/10にして0.1を書き込み、キャラのハイライトは10を1/10にして1 を書き込みます。

そうすれば、「0.1より大きい所だけ取り出してぼかす」ということができます。 そして全部処理が終わってから、結果を10倍すればいいのです。

「そんなことができるならそうすればいいじゃん」と思われるかもしれません。 しかし、これには犠牲が伴います。 まあやってみましょう。

10分の1で描いて、終わってから10倍

まず、背景のテクスチャの色を1/10します。 テクスチャをいじるのは面倒なので、マテリアルカラーを乗算して1/10にしましょう。

f:id:hirasho0:20191003165337p:plain

キャラの方はライトの強度を1/10にします。

f:id:hirasho0:20191003165334p:plain

環境光(アンビエント)も1/10にする必要があることに注意してください。 Unityの場合LightingSettingsで環境光の強度を下げます。

f:id:hirasho0:20191003165329p:plain

さて、この結果、

f:id:hirasho0:20191003165357p:plain

ブルームなしでこういう絵になります。ほぼ真っ暗ですが、 キャラのハイライトだけは白く残っていますね。期待が持てます。 実は照明計算を行うシェーダでは、計算結果は10とか100とかになっており、 1以上にならなかったのはキャンバスがLDRだからです。 ライトの強さを1/10にしても、100が10になるだけで、まだ十分明るいので、 こうして明るい色が描き込まれることになります。

では、この状態で、0.1以上を取り出してぼかす設定にしてブルームを有効化してみましょう。 私のブルームにはカラーフィルタ機能がついていまして、 処理が終わった後に何倍かすることができるので、それを使います。

f:id:hirasho0:20191003165332p:plain

ColorScaleに10を入れてあり、これで10倍になります。 ブルームの強度はほどよく調整して(この例では4くらい)みました。

f:id:hirasho0:20191003165344p:plain

確かにハイライトだけが光ってはいて、背景はそのままなんですが、 この四角いガタガタしたのは何なんでしょうか。

これが先程言った「犠牲」です。犠牲にしたのは、「精度」です。

元々LDRの場合、0から1を256段階で表していました。 1/10して描き込む場合、本来の0から1の範囲は段階が10分の1になってしまい、 たった25段階になってしまいます。ハイライト以外の所も色が段々になっていて 汚ないのがおわかりでしょうか。たった25段階しかない状態にしたものを、 後から10倍すれば、汚ないのは当然です。

妥協

自然界の物の明るさの幅というのはすごいものがあって、 そのあたりの物と太陽の明るさの比は10倍や100倍どころの話ではなく、 万とかの桁になります。 普通に見えているものと、光っている電球でも100倍くらいは違いがあり、 本当は10とか100とかが入っている所だけをぼかしたいのですが、 精度のせいで10倍すらままなりません。

仕方ないので、2倍で我慢しましょう。 明るさを1/2にして描画し、後で2倍して戻します。 先程0.1を入れた所を0.5にし、 10を入れた所は2にします。

f:id:hirasho0:20191003165340p:plain

実は、これが冒頭のスクリーンショットです。

0から1に128段階しか使えないので結構精度は落ちており、 マッハバンドも出てしまっていますが、 LDRで無理矢理やるなら我慢するしかないでしょう。 もっと凝った手もあるにはありますが、負荷が重くなるので 意味がなくなってしまいます。

まとめ

今回紹介したのは、LDRでブルームをやる一番安い方法だと思います。 「何分の1かの明るさで暗く書いて、いらない所が光らないような値を設定してブルームし、明るさを元に戻す」 という手順です。現実的には精度の問題があって2倍が限度でしょう。

さて、安いとは言え、後付けで導入するとなると問題は簡単ではありません。 最低限、テクスチャをいじらないで描画する明るさを変えられる必要があります。 テクスチャを直すコストは到底許容できません。 標準のUnlit/Textureシェーダは乗算色を指定できないので、これで作っていると シェーダの差し替えから必要になってしまいます。

また、乗算色を指定できるシェーダを使っていてテクスチャを直さずに済むとしても、 今回の例のように「照明計算なし(Unlit)」なものがあると、マテリアルの個別設定が必要になります。 その意味で、アニメ的な背景や、加算で乗せるエフェクトなどは厄介です。 エフェクトについては別カメラに分けて「エフェクトを描画する前にポスプロを済ませる」 という手もありますが、背景はそうも行きません。 まあ背景はそんなに数はないでしょうけれども、やっぱり手間は手間ですよね。

ところで、ここまでLDRでがんばっておいて何ですが、 「HDRにしちゃう」という選択肢は当然あります。 ブルームをやるならそれが一番素直な選択肢です。 高級なメモリを積んでいて、キャンバス(レンダーテクスチャ)の容量が増えても あまり遅くならない機械であれば、それでもいいはずです。 機械の性能を何らかの手段で判定して、 HDRに切り換える手はあるかと思います。 カメラをHDRを使うように設定して、 標準のPostProcessingStackを使えばいいでしょう。 性能が余っているなら妙な工夫は不要です。

ただ、「HDRの時はアンチエイリアスが効かない」といった制限がある機械も 過去にはありました。ある程度の数の機械で試して、 性能や機能制限を見ておくのが無難かと思います。

UnityEditorからスマホに直転送して確認したい

f:id:hirasho0:20191003164720p:plain

こんにちは。技術部平山です。

今回は、スマホでhttpサーバを動かした話 の続編でして、UnityEditorから直接スマホにデータを送って即確認する仕組みのお話です。 コードはgithubに全て置いてありますが、まだ実製品の開発には投入しておりません。 現状はプロトタイプです。

今回作ったものを使う流れ

  1. スマホに今回用意したアプリをインストールして起動しておきます。
  2. 今回作ったエディタ拡張ウィンドウに、スマホ側のIPアドレスとポート番号を入れます。
    • IPアドレスはスマホの画面上に出ています。ポート番号は本サンプルでは8080です。
  3. エディタのプロジェクトビューで、スマホで見たいプレハブを選択します。
    • 図ではKonchというプレハブが選択されていますね。
  4. 「選択してるものを送る」ボタンを押します。
  5. スマホで絵が出ます。MonoBehaviour.Start()で再生開始するアニメーションが入っていれば再生されます。

スマホ上の操作は以下のような感じです。

  • タッチ操作で回転、拡大縮小
  • Playボタンを押すとインスタンス生成からやり直し
  • Stopでインスタンス削除
  • Nextを押すと次のプレハブへ移行(複数送った場合)

選択するのはプレハブでもいいし、プレハブを含んだフォルダでもかまいません。 依存したアセットも一緒に転送されるので、単にプレハブだけを選ぶのが 一番使い勝手が良いかと思います。

おまけ機能

スマホ側に解像度が書かれたボタンがありまして、 これを押すと、解像度の縦横を1.414(sqrt(2))分の1にして、総画素数を半分にします。 「ピクセル処理が重いせいでフレームレートが落ちている」かどうかは、これですぐわかるわけです。

さらに進めて、UnityEngine.Profiling.Profilerを使って、スマホ側でプロファイリングを行い、 データをwifiでエディタに送る、ということも考えていますが、 使うのは主にアーティストさん、あるいは企画さんですから、 「シェーダ重い」「マテリアル多すぎ」のように一行でわかるレポートを出す方が、実用性は高いでしょう。 そのあたりは今後考えます。

注意点

  • UnityEditorのPlatformはつなぐ機械に合わせてください。iPhoneを使うならiOSです。でないと互換性がないデータを送ってしまいます。
  • スマホとPCは直接通信できる必要があります。ネットワークが分かれていて通らない、といったことがあると使えません。

何のために作った?

開発の反復速度(イテレーション)を上げるためです。

もちろん、Unityを使えば、作ったものはUnityEditorの画面上で見られます。 シーンビューなら即座に見えますし、 ゲームの流れの中でプログラムが入った状態での見え方を知りたければ、 再生してゲームビューで見ればいいわけです。 ですので、いちいちスマホに持っていって確認する必要はそんなにはありません。

ですが、スマホ実機で確認した方がいいことや、実機で確認せざるを得ないこともあるのです。

  • 処理負荷
    • 実際どれくらい重いのかは実機で動かさないとわかりません。
    • 今回のサンプルでは処理負荷のメーターも出しておきました。重ければすぐわかります。
  • 解像度の適正さや、サイズによる印象の確認
    • PCのモニタは大きいので、無駄に解像度を上げたくなってしまいがちですが、実機は小さいのでそんなに解像度がなくても気にならないことは多いはずです。
  • ハードウェアの個性の確認
    • たまにハードウェアの個性で違った見え方をすることがあります(バグとも言う)。Zバッファ書き込み制御が効いていないように見える機種を見たことがあります。

実機での確認は手間がかかるので、やらずに済む方がコストはかかりません。 「実機で確認するのがプロの心得」と言う人もいますが、 私は「どこまで実機確認せずに済ませられるか詰めてこそプロ」と思っていますので、 たぶん仲良くはなれないでしょう。 そんな私でも、実機確認が必要な場面があることは認識しており、 どうせ必要ならできるだけコストを安くすべきだ、と思うわけです。

これがなかったらどうなる?

PCからスマホに直接転送する手段がない場合、 代表的な確認の方法は二種類あるのではないかと思います。

アプリ本体にデータを入れてビルドする

原理的にはプログラマなしでもできますが、 アプリ本体のプロジェクトの適切な場所にデータをつっこむ必要があるため、 プログラマの手が必要になるケースも多いでしょう。

受け渡しにgitを使えば、gitの操作も必要になりますし、ビルドにjenkinsを使う場合はその手続きも必要になります。手順が効率化され、ビルドを高速なマシンでやるとしても、待ち時間を数分より短くするのは難しいでしょう。典型的には30分くらいはかかるのではないかと思います。

アセットバンドルをビルドしてサーバに配置する

アセットバンドルのビルドを行って、アプリからアクセスできるサーバに配置します。 ビルドをアーティストのPC上でやるか、一旦git等に上げてからjenkinsでビルドするかは 選択肢がありますが、できたものをサーバに上げる手続きが必要です。

プログラマの手を介する必要はないでしょうし、アプリのビルドほどの時間はかからないでしょうが、 それでも1分以内、というわけには行かないかと思います。

で、これをどう使うか?

今回のサンプルは、あくまでサンプルです。 実製品では アプリ本体に入っているスクリプトがないと動かないケースも多いでしょうし、 どのような機能が必要になるかは製品によります。

例えば、ライティングを朝と夜で切り換えて具合を見たいとか、 いくつかある品質設定を切り換えた時の処理負荷が見たいとか、 他のゲーム要素と一緒に出して見たいとか、 そういったことはいくらでも考えられます。

ですから、今回作ったものをそのまま使うことは想定しておらず、 現実的には、アプリの中に専用シーンを用意するか、 専用アプリを用意するか、といった選択肢になるかと思います。 専用アプリを用意するのは、 おそらく本体とアセットバンドルを別プロジェクトに分けているケースでしょう。 アーティストはアプリ本体でしか使わないスクリプト群を必要とせず、 プログラマの大半はアセットバンドル素材には触れないので、 分けた方が安全かつ効率的です。

アーティスト向けツールなどもアセットバンドル側プロジェクトに入れるのが おそらく良く、今回のツールもその例になるかと思います。 アプリ本体と同じスクリプトが必要なケースでは、 本体からパッケージをエクスポートして入れることになります。

実装

エディタ側は、選択されたものをこっそりアセットバンドル化して、 UnityWebRequestでスマホ側のサーバにPUTします。

スマホアプリ側には、以前紹介したhttpサーバが入っており、 ファイルアップロードのイベントで実行されるコールバックで、 アセットバンドルをロードし、中のプレハブを見つけてInstantiateします。

雑に言えばそれだけです。仕組みとしては難しいことはありません。 ただ、結構面倒くさいところもあります。

切断対策

オフィスの1階から3階に上がると何故かつながらない、 といったことが結構起きます。 違うルータに切り換えるにあたってwifiが一時的に切れたのでしょうが、 切れた後しばらく調子が悪いこともあります。アプリを再起動したり、 wifiを一度offにしたりしないといけないこともありました。

詳しいことはおいおい調べますが、 いずれにせよ、エディタ側、スマホ側の両方に「ネットワークの接続状態」 を表示しておかないと、「なんか動かない」となって使い勝手を 悪くしてしまうことが想像されます。

そこで、エディタ側では5秒ごとにスマホのAPI(pingという何もしないapiを用意してある) を叩いて、接続を確認するようにしています。これはもっといい手があるかもしれませんので、 ネットワークに詳しい方に教えを乞いたいところです。

スマホ側はサーバですので、単純にApplication.internetReachability を見て、定期的に画面表示を更新しています。 つながっていればIPアドレスを表示し、wifiが切れていれば「NO WIFI」と表示します。 この手のツールで4Gの回線を使うのはどうかと思うので、 wifiが切れていれば使えない、としています。 LANのIPアドレスを取るのは以下のようなコードです。

public static string GetLanIpAddress()
{
    string ret = null;
    foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
    {
        if ((ni.NetworkInterfaceType == NetworkInterfaceType.Wireless80211) || (ni.NetworkInterfaceType == NetworkInterfaceType.Ethernet))
        {
            foreach (var ip in ni.GetIPProperties().UnicastAddresses)
            {
                if (System.Net.IPAddress.IsLoopback(ip.Address))
                {
                    // 無視
                }
                else if (ip.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
                {
                    ret = ip.Address.ToString();
                }
            }
        }
    }
    return ret;
}

速度が出ない

前にhttpサーバの記事を書いた時点では、たかだか数KBのファイルしか 送っていなかったので、速度のことは全く考えていませんでした。 しかしメガバイト量のデータを送ると数十秒かかることがわかり、 処理を見直しています。

具体的には、PUTを受け付けてファイルをpersistentDataに保存する処理を、 同期処理から非同期処理にし、CPUが無駄にスピンロックしないようにしました。 メインスレッドが休みなくwhileで回ってしまうと、 別のスレッドで動いているhttpサーバ処理も遅くなってしまうようです。

まだ秒間1MBも出ないので遅いのですが、 とりあえず使えなくもない速度になったかと思います。 より高速化する手法をご存知の方は是非教えてください!

作ったアセットバンドルをPCで見るとピンク

Android用のアセットバンドルには、Androidでしか使えないシェーダが入っています。 Android用のテクスチャはPCでも使えるのに、シェーダはダメなのです。 アセットバンドルにマテリアルが入っていて、 そのマテリアルが使っているシェーダがアプリ本体にしかない場合、 アセットバンドルの中にシェーダのコピーが入ります。 アプリ側のシェーダを変更してもアセットバンドルに入っている方は変わらないので 事故の元になる、というのはまあ別の話ですね。

さて、今回の仕組みをデバグするにあたって、 Android用のアセットバンドルをPCでも 見えるようにしたい、という欲求が出てきます。 ビューアアプリをエディタで再生状態にして、PCのIPアドレスを指定すれば 動くのですが、シェーダだけはダメです。ピンクになったり環境光が当たらなくなったりします。

というわけなので、エディタ実行時にはマテリアルの中のシェーダを PC側のシェーダに差し換えることにしました。

material.shader = Shader.Find(material.shader.name);

これだけです。InstantiateしたGameObjectからRendererを全部得て、 マテリアルを全て取り出し、Shader.FindでPC側のシェーダを見つけて差し換えます。

おまけ:高速化

この処理は、大量のマテリアルがあると、かなりのメモリと処理時間を食います。 また、マテリアルプロパティが数多くある複雑なシェーダだと、 マテリアルプロパティを一つづつコピーする処理が必要らしく、 べらぼうに重くなるのです。

エディタだけなら気にしなくてもいい、とも言えるのですが、 実製品では200MB以上のメモリと4秒以上の時間がかかったりもして、 ちょっと悲しかったので対処しました。 今回のサンプルにはこれが入れてあります。 ImcompatibleShaderReplacerという、英語が不安な名前のクラスでして、 これのReplace()を呼ぶと、エディタ実行時のみ再帰的にマテリアルのシェーダを差し換えてくれます。

その際に、「すでに差し換え処理をしたマテリアルはもう差し換えない」 という処理を足すことで、無駄な差し換え負荷を防いでいます。 素直にHashSetにMaterialを登録していくと無限にHashSetが 肥大化していって、そこに格納されたMaterialがメモリリークになるため、 固定の配列を用いて、一定数以上に増えないようにしてあります。

終わりに

いじっては確認、いじっては確認、というサイクルが速く回ることは 何にも増して重要です。 待ち時間が長いとサイクルを回す回数が減って質が落ち、 待ち時間のためにやる気が失せます。

今回はサイクルを早く回すための方法を一つ試してみました。 今後は特定の製品向けのカスタマイズを行って、実戦投入していく予定です。 そこで得た経験は、今回のサンプルにも反映させていくことになるでしょう。

なお、製品向けカスタマイズをするまでもなく、必要とわかっていることはいくつかあります。

  • ライト制御
    • ライトも当然動かしたい
  • hierarchyとinspectorに相当するものをエディタに表示して閲覧、値の変更をできるようにする
    • 特定のオブジェクトを出したり消したり動かしたりしたいはず
    • コンポーネントのパラメータも見たいしいじりたいはず
  • canvas対応
    • 現状canvasの下にInstantiateされる前提のprefabだと何も出ない。そして2D的に表示する機能もない。

このあたりができた時点で、また記事にするかもしれません。また、 プレハブだけでなく、シーン丸ごと、メッシュ単体、テクスチャ単体、 といったものも見られると良いのかもしれません。

ところで、UniteでUSBで実機と通信して似たようなことをする話 を聞きまして(106ページ目あたりから)、それもアリだなと思いました。 ネットワークに起因する面倒は結構ありますし、 IPを打ち込む手間もあります。 USBでつながっていれば、それを第一選択とするように拡張するのは良さそうです。