ひらしょー

平山尚が技術のことを書く場所。Unityが多そう。

Unite Tokyo 2018

Uniteに参加してきたのでメモをしておく。

さては非同期だなオメー!async/await完全に理解しよう

http://events.unity3d.jp/unitetokyo2018/session-lineup.html#session53

 

非同期処理をどう書けばいいかがまだよくわかってないので聞いておいた。

 

関数にasyncとつけた上で、中でawaitの後ろにGetAwaiter()を持った

ものを書くと、そこで待ってくれる。

コンパイラはIEnumerableの時同様に関数をバラバラにして、

ローカル変数を保持するためのクラスをでっちあげてくれるわけだ。

 

C#の標準では勝手にスレッドにブン投げて並列化してくれるが、

unityでやる分にはシングルスレッドになる。

それでもTask.Runを使って明に他のスレッドにブン投げて、

そのハンドルをawaitすれば裏に飛ばせる。

受信したjsonを裏でデシリアライズする、というようなことはそれでできる。

 

さて、async/awaitを使うことがおいしいケースは何か?

 

使ったことがないのではっきりとはわからないが、

おそらくは戻り値だろう。

IEnumeratorやIEnumerableを返す関数は、

戻り値を持てない。やむをえず、

 

class RetVal{ T result; }

IEnumerator SomeFunc(RetVal ret){ ... }

 

みたいなことをして引数に戻り値や完了状態を返すか、

最後に返したIEnumeratorから結果を取れるような凝った細工をするか、

という話になる。私は前者の方がシンプルなので好みだ。

 

しかしasyncをつけた関数は普通にモノを返せるので、

そんな必要はない。そして、

IEnumeratorという本来非同期処理のためにあるわけでもないものを

無理矢理非同期処理に使う必要がなくなり、

IEnumeratorを返せばコレクション、asyncがあれば非同期処理、

と見た瞬間に種別がわかりやすくなる。

 

とはいえ、すでにして大量にyieldで書かれた非同期処理があり、

2018に移行するのはもう少し先だと考えると、

焦って使う必要はたぶんない。

 

なお、Task.Runで実行するとプロファイラでは見えない。

Profiler.BeginThreadProfilingを呼ぶTaskScheduler

自作すれば見えるようになるそうだ。

 

そのうち標準で入るようになるだろうし、

自分でやっても大した手間ではあるまい。

 

 スクリプトによるTimelineがっつり拡張入門

http://events.unity3d.jp/unitetokyo2018/session-lineup.html#session56

 

そもそもTimelineという機能を知らないのだが、

Animationの置き換えだろうと推測する。

カーブ定義して出力するパラメータを決めて再生制御する、

という点では同じだが、

Animationには使いにくい点が多々ある。

いちいちAnimatorを用意しないといけないあたり、

UIを動かすために使うような用途には甚だ向かない。

APIを見ていないので確たることは言えないが、

Timelineならそういう用途にもおそらく使えるだろう。

エディタ側のUIの設定に関しても、再生制御にしても、

いじれる余地が大きそうだ。

 

さて、現状私が日常目にするUIその他の動きをつける方法は5つある。

- Animationの利用

- DOTweenの利用

- Updateベタ書き

- AfterEffectsからエクスポートしてスクリプト

- Spine

 

昔はAnimationを使って作ることを模索した時期もあったが、

最近はめっきりやらなくなった。GameObjectとのバインドが

パス文字列なので、一つGameObjectをはさんだだけで全部切れてしまう。

あとassetが増えるのが邪魔くさい。結局なんだかんだ言って、

何か作ればクラスを定義することになるわけで、

Animationだけ作って差せばいいというわけには行かない。

調整もスクリプトだけで完結しない。

そもそも私はそもそも細かくキーをいじって

アニメを調整するようなことはなく、

Animationの利点をあまり得られないのである。

もし細かく調整するならAnimationでやる方が

早く反復できるので、その他の方法より良いだろうと言える。

しかしそれはアーティストがやる方がいいと私は思っている。

 

次はDOTWeenで、これならスクリプトで完結するし、

簡単なものならすぐ書ける。

しかし、簡単なものでないとコードが意味不明に複雑化するし、

多量に発生するものに使うと性能的にヤバい。

画面に同時に10個とか出て、連打するとその度に消えたり出たりする、

というようなものの制御にこれを使うのは悪夢だ。

アニメーションの定義と再生インスタンスが割れてないので、

再生する度にアニメーションの定義コードとメモリ確保が走ることになる。

また、関数が呼ばれる度にSequenceのインスタンスを作るような場合、

前に起動したものを殺し忘れるとひどいことになる。

 

次がベタ書きで、これはUpdateの度にdeltaTimeを使って

手動で動かす方法だ。「0.2秒かけて現れる、消える」

みたいな単純なものならこれで書ける。

ただし、時刻や状態を保持するメンバ変数を用意して

管理する手間がかかり、やるアニメの複雑度が上がると

一瞬にして手間とコード規模がふくれ上がる。

ただし、ガベコレは一切発生させないで書けるし、

Updateでなく自作の更新関数でやればオーバーヘッドもない。

私は結構これもやる。

 

そしてAfterEffectsから吐いたものを自動変換して作ったコードで、

コードなのでできたものは上の「ベタ書き」に近い。

ただしカーブを表現するデータを多量に初期化したり、

カーブとgameObjectをバインドしたりするオーバーヘッドがあるので、

ベタ書きほどは速くないし、初期化ではメモリを食う。

そして、生成されたコードはもはや人が読めるものではなく、

コードなのでAssetBundleにも入れられない。

しかし、AfterEffectsで作ったもので、しかも垂れ流しで良いものであれば、

どんなに複雑なものでも同じ手間で再現できる。

そしてアニメーション定義と再生インスタンスが別なので、

同じアニメを多量に再生する場合の負荷やメモリは軽く済む。

ただし、対応していない機能を使われているとそこだけ手書き対応になって

手間が跳ね上がる。現状マスクやトライトーンは対応しておらず、

とりわけシェイプを使ってマスクをやられるとえらい手間がかかる。

まあそれはAnimationを使っても同じだろうが。

 

最後がSpineだが、これはあんまりUIには使ってない。

再生ランタイムがMeshRendererとして描画するので、

UGUIの中に挟みこめないからだ。

しかし完全にデータ化されていて、アセットバンドルに入れられるので、

こっちを使う手もある。あるいは再生ランタイムを自作したり、

いじったりしてUGUI化することもできるだろう。

 

さて、Timelineの話だった。

これをどこで使いたいかと言えば、当然AfterEffects再現だろう。

現状コードで吐いているものを、Timelineのアセットとプレハブにすれば

AssetBundleにも入れられるし、C++の実装によって高速化することも

期待できるかもしれない。

ただし、

「このノードの下に別のプレハブ差す」

「AE上では画像だが、実際はTextにする」

といった改変は日常茶飯事で、Timelineまで持っていけるケースが

どれくらいあるかはやってみないとわからない。

 

なお、現状私がいる場所ではデザイナーがUnityで作業する、

ということがほぼない。もしそれがあるなら、

最初からTimelineで作られるはずで、

そうなれば話は全く変わってくる。

 

60fpsのその先へ!スマホの物量限界に挑んだSTG「アカとブルー」の開発設計

http://events.unity3d.jp/unitetokyo2018/session-lineup.html#session63

 

2000個弾が出る縦シューをどうやってUnityで作るか、という話。

C#の最適化の話はよくわかる。

Normalizeの中でmagnitude呼んでるとか許し難いだろう。

しかし幸いにして私が作っているものは

そういう所を気にしてもほとんど影響がないので、

Vector2やVector3をそのまま使っている。申し訳程度にrefをつける程度だ。

[,]を使うか[ ][ ]を使うか、structでforeachするコスト、

そういったものはほとんど問題にならない。

他にいくらでも重い処理がある。

 

ただし、デバグ描画ライブラリでは千文字以上画面に出たりする関係で、

描画負荷自体は弾が2000個出るのとさほど変わらない状態になるので、

デバグ描画ライブラリの中は結構気を使っている。

例えばVector2の関数は使わない。ベタ書きだ。

refがつけられる時はつけるし、不要にclassを使うこともない。

モノをたくさん出すゲームならまた違うのだろうが、

今はそういう状態だ。

 

とはいえ、スパイクについては考えさせられた。

GCに関してはやれる範囲でとしか言いようがないが、

シェーダコンパイルとテクスチャのVRAM転送に関しては

確かに手を打っておいても良い気がする。

OpenGLでは実際に画面に絵が出ない限りテクスチャがVRAMに

送られず、初めて絵が出るフレームにスパイクが出る、

というのは知っていたが、まさか2018年になってそんな話を聞くとは

思わなかった。今起こっていることが

Unityのせいなのか下のGLのせいなのかは知らないが、

確実な方法が「一回描画すること」しかないことに違いはない。

各アトラスについて、それを貼ったポリゴンを何かしら

描画したらいいんだろう。

しかし、動的にロードされるエフェクトの類では打てる手はなさそうだ。

全種類前もってメモリに乗せるわけにも行かない時もある。

 

そういえば、移動経路の曲線データを事前に分割した直線データに

変換していたのは面白かった。

おおまかに言って、時代が進むほど、計算と転送は計算の方が安くなる。

昔は事前計算して読み込んでいた方が速かったものが、

その場で計算する方が速くなる、というようなことだ。

なにせ2018年なので、スマホはストレージやメモリ

からの転送の方が計算よりも高くつく公算が大きいハードだろうと

勝手に思っていた。それだけに、ベジェの計算程度でも事前計算の方が速い、

というのはちょっとショックだ。そんなにCPU遅いのか。

 

ScriptableObjectの利用について。

会話シーンの類はUnity上に編集環境作って

ScriptableObjectにしてるらしい。

企画にjsonやらxmlやらエクセルやらを作らせて、

それを何かに変換してプログラムにロード、

という作りだと、どうにもイテレーションが遅くなる。

Unity上でやってもらってその場で保存してもらうのは自然だろう。

しかし、企画やアートにとってUnityが手に馴染む良いツールになるのか?

というのは未だに確信が持てない。

エクセル編集でも実行したまま再ロードができる仕組みがあれば、

それはそれでアリだ。もちろん実行中の再ロードができなかったり、

サーバの再起動が必要だったりするフローは論外だが。

 

P-MAPというものを使うと、加算と通常合成が交互に来ても

1パスで書けるらしい。どうもテクスチャを事前に加工してるらしい。

同じシェーダと同じブレンドモードで行けるということで、

一体何をしているのだろうか。

 

通常合成は、D' = S*Sa + D(1-Sa)

加算はD' = S*Sa + D

だ。

ブレンドモードがSa, 1-Saであれば前者、Sa, 1であれば後者になる。

これを同じにしたいと。ここでSにSSa=S*Saを入れておき、

通常合成はSaをそのままにし、加算ではSaを0にし、

ブレンドモードを1, 1-Saにすれば、

加算合成も加算もD' = SSa*1 + D(1-Sa)

となって同じブレンドモードで扱える。

シェーダ内で分岐して吐くアルファを0にするかSaにするかを決めてもいいが、

それではシェーダに分岐が入って重くなるしマテリアルが割れるから意味がない。

その情報を入れるなら頂点かテクスチャかだ。

アトラス化も兼ねてテクスチャでやるアプローチの方がおそらくは普通だろう。

しかしテクスチャに触りたくないケースでは頂点という可能性も残る。

おおよそ話はわかった。

Unityが透明部分に勝手な細工をしないようにAlphaIsTransparencyは

切った方がいいだろう。

 

 運営中コンテンツにおける大型アップデート成功のための考え方とUnity最適化手法 

http://events.unity3d.jp/unitetokyo2018/session-lineup.html#session66

 

テクスチャに影描きこんで照明計算なしで出したゲームを、

後から照明計算を足して豪華な絵にしよう、という話。

 

解像度を上げて、フルスクリーンアンチエイリアス(MSAA)をかけ、ポスプロを足し、

テクスチャに含まれていない成分である環境光と鏡面反射を足している。

影が描いてあるならディフューズ項はいじれない。

とはいえ、場所依存の環境ディフューズの成分は描かれていないだろうから

足しても問題は少ないだろうし、

描いてある影のいくぶんかはアンビエント遮蔽だろうから、

それはそのまま乗算してもそれっぽくはなる。

 

ただ法線が問題だ。元々アウトライン用のウソ法線しか入れていなかったそうで、

本物の法線を入れねばならないのだが、FBXには法線が2つ入らない。

UVのフリをして入れて、インポート時にタンジェントにつっこんだとのこと。

なかなか手間だ。

 

ポスプロはZバッファ生成パスを用意してそれを使ったそうだが、

よくわからない。通常描画で描いたZは使えないのだろうか?

もしそうなら、私だったらZを使うポスプロはあきらめたくなる。

ライトシャフトだろうか。

 

豪華版は影描画も追加。

Unityの標準だとモバイルはハードシャドウになって許容できないので

自作したとのこと。物を見ていないのでわからないが、

普通のデプス影でソフトにするには結構な計算量がかかる。

プロジェクションシャドウと言っていたが、

地形だけに落とす奴だろうか?

 

出してしまったゲームの画質を後から上げる、

ということはスマホの場合には結構あるだろうし、

そもそも出した当初から機械の性能には天地の開きがあり、

機械によって使う技法を切り換えるのはありうることだ。

良い機械を持っている人にはより良い体験を、

というのは作り手の精神として共感できる。

ただ、それを割に合う手間で、というのがなかなかに厄介なのだ。

 

まず、解像度とフレームレートに関してはほぼタダでできる。

良い機械では解像度が上がり、フレームレートが上がる。

これは自然にできる。

性能の割に解像度が低い端末であれば、

例えば2倍解像度で描画して縮めたっていい。

単純に2倍重いが、MSAAなどより遥かに綺麗だ。

MSAAは機械によって負荷がどうなるかが読めないところがあるし、

綺麗になるのはポリゴンエッジだけなので、

安く出来るチップでない限りは割に合わない印象がある。

そのチップがどんな特性のチップかがわからない状態でMSAAは

あまりやりたくない気がするが、

多数の機械で試しているのであれば問題ないだろうし、

なにせスイッチを入れるだけで有効化できるので手間では圧倒的に有利だ。

ただ、centroidサンプリングを選択できないと、

おかしなテクセルを拾って絵がおかしくなる。

実際そうなっていたらしく、テクスチャのアトラス境界を広げたそうだ。

モバイル向けのUnityではcentroidサンプリングはできないのだろう。

ますますMSAAを使いたくなくなる。

コンピュートシェーダが使えるマシンでだけそれを使ってAAをかける、

というのではダメだろうか。

 

元の状態でスペキュラがなかったのであれば、

人間は物の形や動きをスペキュラで把握する傾向があり、

スペキュラを足すのは効果が大きい。

しかし、スペキュラをまともに足すにはマテリアル設定が必要で、

何をどう考えても手間が増す。

元のモデルがマテリアル境界で分割されているとは思えないわけで、

粗さや反射率をテクスチャにつっこむことになるのだろう。

これも結構な手間だ。

そして、スペキュラは法線マップとセットでないと効果が薄い。

頂点が少ないとディティール感が出ない。

だが法線マップを貼るにはタンジェントが必要だ。

さらに手間が増す。これは頭が痛いな。やりたくない。

 

テクスチャはETC2にしたらしい。

どうせ低性能機種で読めないならASTCでもいいんじゃないか?

と思うが、単に「高級機種だけ綺麗にしたい」

というわけではなく、より多くのマシンで改善したかったのだろう。

ETC1に比してマッハバンドが改善しているそうだ。

どうも圧縮のQuality設定を上げればETC1圧縮でもマッハバンドは

消えるようなのだが、べらぼうに圧縮が遅くなって

パイプラインに悪影響があるので、ETC2だとそこそこのQualityでも

マッハバンドが消える、ということを重く見たとのこと。

更新頻度と物量次第だろうとは思うが、

ローカルではQuality0でまずインポートし、

土日に自動でQuality0のものを100に変えてインポートしなおせば

我慢できなくもない気はする。

キャッシュサーバも助けになる。 

それで済むならETC2のためにUnityのバージョンを上げる必要もなかっただろう。

おそらくはそれで済まない理由があったのだろうと思う。

 

では、そのクラスの大型更新をどうやって確実に遂行するか。

まずチームは割り、運営チームの邪魔をしないことを最優先する。

完全に作業が終わるまでは運営側のdevelopには一切入れない。

更新用のdevelopにちょくちょく運営のdevelopをマージしつつ開発を進める。

#if で古いUnityと新しいUnityを分岐させる時はシェーダは注意せよとのこと。

自動でシェーダを新しいUnity用に描き換える機能があるらしく、

それが#ifを無視するそうだ。切っておかねばならない。

 

さらに、AssetBundleにシェーダが入ると互換性がない。

古いUnity用のシェーダが入っていると新しい方で読めず、その逆もある。

ABにシェーダが入っているか調べるツールを書いたそうだ。

これは他人事ではない。

シェーダが足りなくて紫になる事故はたまに起こる。

 

ABビルドは並列ビルドしてるらしい。

ビルド済みのManifestがあればビルドしないので、

Manifestをいい具合にコピーしておいてビルドを抑制すると。

manifestがなければ、それに対応するABがビルドできると。

後から別の人に聞いた話では、

実際やってみるといろいろ罠があるらしく簡単ではないそうだが、

しかしやってできないほど難しいことではないだろう。

ABのビルド時間が問題になるなら試してみても良さそうだ。

 

 

『CARAVAN STORIES』のアセットバンドル事例

http://events.unity3d.jp/unitetokyo2018/session-lineup.html#session72

 

正直衝撃の連続だったのだが、それは私の経験が浅いからだろうか。

 

まず「アプリが2GB越えしてからAB化した」というのにド肝を抜かれた。

ビルドは3時間だそうだ。

AB化にはビルド時間の分散と短縮という意味もある。

ビルドの度に全アセットの変換を行うよりは、

こまめにAB化して、その後は触らずに済む方が合計時間は短くて済む。

 

一体日々のテストプレイはどうしていたのだろうか。

2GBのapkを皆に配っていたのだろうか。

実機でのテストは一体どれくらいしていたのだろうか。

実機でないと感じがわからないことは多々あるだろう。

その度に3時間待たされたのだろうか。

どのようなフローで開発しているのか正直想像がつかない。

 

また、同期ロードを非同期ロードに変える工数を嫌った、

というような話があったが、

正直私の感覚だと「同期でファイルIOをやって許されるのはデバグ時だけ」

なので、それも衝撃だった。

最初から全てが非同期であれば何ら手間などかからないし、

いずれアセットバンドル化することは明らかなのだから、

Resourcesから読むかABから読むかは最初から抽象化しておけば良い

ように思える。ベータに突入してから、

極力影響が及ばないようにその作業をする、

というのは一体何が起こっているのだろうか。

 

しかし、私はMMOなんて作ったことはないし、

スマホで物を売ったこともない。

おそらくは、私に見えていない何かがあって、

そこに妥当性があるのだろうと思う。

 

それにしても、先にResourcesを見に行って、

なかったらABを読む、というのはレイテンシが心配だ。

ファイルの存在非存在だけでもシステムコールを叩くわけで、

多少はかかりそうに思える。

 

データのunload2秒待ってからにしてるらしい。

次に同じものを使う時に再度ロードのレイテンシがかかるのを

防ぐためらしいが、それなら適当なキャッシュシステムを

用意する方が確実な気はする。

例えば50MB分キャッシュすると決めておいて、リストにつなぎ、

使った時にリストの先頭に動かす。容量があふれたら、

末尾から順に削る。

こんな簡単な処理でも、即座に再利用した場合にはロードが走らなくなる。

しかしそれが面倒だったということだろうか。

 

ABにしたらアニメーションのロードが速くなった、

という話は意味がわからなかった。

Resourcesから31個のAnimationClipを読むのに6秒かかっていたが、

それが一瞬になったという。6秒もかかること自体ありえないと思うのだが、

テキストとかバイナリとか言っていた気がする。

まさか、Animationの保存形式にテキストとバイナリを選択でき、

テキストにしていたが、ABにビルドする際にはテキストの選択肢がないので

バイナリになって高速化した、ということだろうか。

知識がなくて意味が取れなかったのでこのあたりの話はよくわからない。

 

最終的にアプリのビルド時間は3時間から5分になったという。

アプリサイズは45MBだそうだ。5分はうらやましい。

アプリ埋め込みのリソースを減らしてABに叩き出せば速くなるのだろう。

専用スクリプトが不要なシーンがあるならば、シーンごとABに

叩き出すこともできるのだろうし、それができれば拡張の点から有利なのだが、

現状そういう作り方をできる目処は立っていない。

いずれ考えたいところだ。

 

ABは4600ファイル。元のアセットは10万個。

4並列でダウンロードする。4並列もやれば、

それくらいのファイル数でもオーバーヘッドが気にならないのだろう。

であるならば更新時の差分を小さくする意味でも、

あまりまとめすぎない方が良いのかもしれない。

ただ、ABのバージョンチェックにはファイル数に比例した時間がかかる

印象があるのだが、そのあたりはどうだったのだろうか。

 

一つ気になる話があった。

音が鳴らないことがあり、UnloadUnusedAssetsで再生中のが破棄された

のではないか?とのこと。

UnloadUnusedAssets後再生が止まったら再度再生して回避しているらしい。

音が鳴らない、という不具合が出てきたらこのことを思い出そうと思う。

 

カスタムシェーダーでモバイルでも最先端グラフィックスな格闘ゲームを!

http://events.unity3d.jp/unitetokyo2018/session-lineup.html#session72

 

大手で社内ライブラリを作っていた人がUnityの台頭で

チームを渡り歩いて最適化をやる仕事をしている、

ということには何となく時代を感じる。

私は下層を作ることをやめてゲームの上層を作る側に転じたが、

この講演の人は過去の経験をそのままUnityで活かしていて、

いろいろな生き方があるなと思う。

 

グラフィクス系の講演であるにも関わらず

一度も絵が出てこなかったことに度肝を抜かれた。

論文への参照があるあたりもUniteの他の講演と違う臭いを感じる。

このような言い方が良いかはわからないが、

「あ、オレも以前あそこにいたんだったな」的な郷愁がこみ上げた。

 

内容としては、

Unityに任せると遅いので昔PS3あたりでやっていたことをUnityでまたやった、

という話と言って良いかと思う。

 

影を描くためにカメラを用意すると諸々遅いので、

メインカメラのスクリプトでコマンドバッファへの描画を発行して影を描く。

標準シェーダの影は遅いので昔ながらのユニフォームシャドウマップを自作。

行列は自前生成するからカメラは不要。

カメラのカリング処理などが全部不要で速い。

 

テクスチャの焼きはPlatformをモバイルに設定するとLDRで焼かれて

話にならないので、Standaloneにして焼いて、

出てきたテクスチャをReinhardでLDR化して吐き出し。使用時に逆変換。

ライトマップはLightmapSettingsから抜ける。RT用意してBlitしてReadPixels。

 

デコードは lightmap / (1h - min(0.9h, lightmap))

最大10で切って無限に飛ぶのを抑制する。

モバイルでもdepth shadowmapはハードウェア支援があるので、

影はdepth shadow。

 

影描画は、影を受けるオブジェクトをリスト化しておいて、

MaterialPropertyBlockで影テクスチャと影フェッチ行列をつっこむ。

こうすればマテリアルを生成せずに済む。

これらの処理を行うのはメインカメラのCamera.OnPreRender。

 

透視変換行列はGL.GetGPUProjectionMatrixで取れるので、

フェッチの際にはこれを使う。Vが上プラスか下プラスかは下層のAPI依存なので

これが必要。

 

照明計算はリニア空間HDR。forwardでMRTなし。

テクスチャはsRGBなので、フェッチ後リニアに変換。

RTへの描き込み前にトーンマッピング

ガンマ変換してsRGBにする。

HDRバッファがあればHDRかつリニアのまま書き込んで、

描画終了後にポスプロでトーンマッピングとガンマ変換を

やる所だが、HDRバッファがないので

それぞれのモデルのレンダリングでバラバラでやらねばならない。

 

トーンマッピング時のEV値はアーティストが設定。

Filmic Tonemapping近似を使用(Hejl 2010)。

 

BRDFの詳細は語られなかったが、

「物理ベース」と言っていたので、おそらくはUEの計算モデルだろう。

私がやるとしても似たようなことをやるのだろうな、と思う講演だったが、

もしそういうプロジェクトがあってもやるのは私ではあるまいな、とも思う。

シェーダをやりたい人は他にいくらでもいるだろう。

 

それにしても会場にいた人は皆BRDFと言われてそれがなんだかわかったのだろうか。

普段Unityを使っていればまず出てこない単語だと思うのだが。

 

Unityの医療と教育への応用 ~ちょっと人を助けてみませんか?~

http://events.unity3d.jp/unitetokyo2018/session-lineup.html#session82

 

脳神経外科の現役バリバリの外科医が、

Unityを使って可視化やシミュレーションを行って、

日々の手術に役立てている、という話。

 

CTのビューアは私も書こうとしたことがある。JSでdicom(医療画像フォーマット)

の解釈をして、webGLにつっこんで描画まではできた。

しかし今の時代それはUnityの方が良いのだろう。

ネイティブになるので速度も出るし、AppStoreその他で配布もできるし、

物理シミュレーションなどもついてくる。

腫瘍が血管を圧迫して血管が本来と違う位置にある、

ということをシミュレートするのにUnityの標準の物理を使っていたりして、

専用に用意しなくても実用になるものが作れるのかと驚いた。

 

医療用のソフトウェアの市場規模は1400億と小さいが、

年率15%で成長しているそうだ。

以前は診断に使われるソフトウェアには厳しい基準があり、

なかなか認可されなくなったが、そのあたりで規制緩和があって

認可されやすくなったとのこと。

確かにフリーで配布されていたCTのビューアも「診断には使えません」

とただし書きがついていた。もしかしたら今は不要になっているのかもしれない。

 

全体的に、患者の固有データをつっこんでシミュレーションしたり、

治療戦略を立てる助けになるようなソフトが足りていないそうだ。

患者個人の脳を立体化したり、変形を加えたりできないと、

治療戦略を立てる役には立たないという。

 

細かい話はいろいろあったが、医療分野を知らない会社との

コラボが良い成果を上げているという話は印象的だった。

それはつまり、プログラマとして優秀な人間が医療分野になかなか行かない、

ということでもあるのだろう。もしかしたらそこにチャンスがあるのかもしれない。

例えば、分析の待ち時間が長くて困る、というような話は、

アルゴリズムと望む結果がある程度わかっていれば、

高速化を得意とする技術者が当たった方が良い結果が出るかもしれない。

患者のCTを撮って3日後に手術、という時に24時間かかるようでは使い物に

ならないわけだ。

優秀なプログラマgoogleやらゲーム屋やらにたくさんいて金を稼いでいるわけで、

医療や学問の世界でプログラマをやろうとは思わないのかもしれない。

 

現状特に時間がかかるのはセグメンテーション処理、

つまり、CTやMRIから立体化する際に、これが脳で、これが骨、これが皮膚、

のように領域分けをする処理だ。そもそも正しく領域分けをすること自体が

困難なのだが、現状のアルゴリズムでも時間がかかりすぎるという。

機械学習を応用して精度を上げた話なども出てきた。

普段実際に頭を開けて手術している医者からこういう話が出てくるというのは

本当にすごい。一緒に何かできる機会があったら面白いのだが。

身内に二人も癌患者が出たこともあって、医療にはかなり関心もあるし。

 

そういえば、一つ現状の用途で役に立っていることとして、

どのクリップを使うと丁度いいかを推測するのに使っている、というのは面白かった。

血管を止めるクリップは使い捨てで数万円する。使ってみてダメだといきなり

万単位でドブに捨てることになるので、前もってシミュレーションで

種類を限定しておきたいそうだ。実際に効果があるのだという。

 

Unityにおける疎結合設計 ~UIへの適用事例から学ぶ、テクニックとメリット~

http://events.unity3d.jp/unitetokyo2018/session-lineup.html#session84

 

疎結合、なんて言葉を聞くと、コードの設計のことかと思うが、

全然違った。いや、全然違う、と言うと正確ではないのだが、

おそらくは「全然違う」と思った人は多かろうと思う。

しかし私にとっては、そんな「疎結合」の話をされるよりずっと面白かった。

というかこのドイツの人、話がおもろい。

 

まず結合という言葉を「片方が変更されるともう片方にも変更が必要になること」

と定義し、疎結合はそのような結合がないこと、としている。

「疎」は「まばら」であって「無」ではないはずなのだが、話としてはわかりやすい。

例えばデザイナーが素材をいじった時に、プログラマに作業が発生すれば

疎結合ではない。密結合だ。

 

どれくらい疎であるべきか、というのは主にプロジェクトの規模によって変わる。

人が多く、期間が長く、仕様が大きいほど、疎にしないと辛くなる。

なるほどそうだ。

 

そして、「何が疎か」ということに関してはレベルがあり、

ワークフローやデータ、そしてコードのレベルがある。

ワークフローにおいて関係が疎であれば、それはデータにおいても

疎であることを要求し、それはさらにコードにおいても疎であることを要求する。

 

デザイナーが直してもプログラマに仕事が発生しないためには、

デザイナーが直したデータをプログラマが受け取る必要がない状態でなくてはならず、

それはそのようなコードによって実現される。

コードにアニメ書いてあるとか、Unityの参照に画像をプログラマが差してるとか、

そういう状態では疎にはなりえない。

 

言われてみれば当たり前なのだが、そのようなことを「疎結合

なる言葉で説明することには意義がある。別に他の言葉でもいいのだが、

大きな概念に名前がつくと応用範囲が広い。

 

ではこの概念上の道具を使ってうちらはどうすべきか?

 

うちのフローは、デザイナーが素材を作ってプログラマが実装する、

この人が言う「ダメなフロー」だ。画像が変わればプログラマ

受け取って差し換えねばならないし、位置が変われば

プログラマがシーン中で置き直さねばならない。

また、デザイナーが仕様を書いて渡すコスト、

プログラマが仕様を受け取って理解するコスト、

といった無駄なコストも発生する。

こういった害は、デザイナーが直す回数が多いほど大きくなる。

直すコストが大きければ直さなくなるので、試行錯誤の回数は減り、

普通に考えて品質に悪い影響が出る。

 

つまり、理屈の上では、デザイナーがUnity上で組んでしまうのが最も良い。

配置も動きもUnity上でデザイナーがつける。

そして、ゲームロジック側とのやりとりは何らかのインターフェイスを設けて

行う。scriptableオブジェクト一個用意して、

そこにsetがあるプロパティを置けば、それはアニメからロジックへの送信になり、

getがあるプロパティを置けば、それはロジックからアニメへの通知になる。

setには「ボタンが押された」のようなイベントや「今のHPゲージの値」

などが入るだろうし、getには「時間が来たからボタンを無効化しろ」

のような制御が入るだろう。いずれもdelegateなりeventなりを置けば、

pullでなくpushにできる。

 

このあたりの作りをこの講演ではメッセージバス、と呼んでいた。

バス、つまり通信チャネル的なものに情報を流し、

相手が誰かもわからないまま送信、送信元が誰かもわからないまま受信する。

こういう作りであれば、UIとゲームロジックを分離でき、

UI単体テストなども可能になる。ゲームを起動しないとテストができないのは

密結合でよろしくない作りだ。

 

実にもっともで、考えさせられることは多い。

 

しかし、それはそれとして、単体テストが不要で、担当者が分かれていない、

というような状況であれば、ゲームエンジンとUIが密結合した

昔の形でも許容できることはあるだろう。

大した回数直さないとか、そもそもデザイナー資源が極端に少なくて

アニメまでつけないとか、アニメをつけるのがうまいプログラマがいるとか、

そういう条件があれば現状のフローも正当化される。

コード的にも切り離せば抽象化層を作る手間はあるし、その分だけ長くなる。

決め打ちであれば不要だった検査類も、分離するなら書いた方がいいかもしれない。

私はここのところ疎結合だったものを密結合に直すという

逆行したことをやっているが、

一旦くっつけ終わったらまた分離を再度考えてもいい気がしてきた。

 

とりあえずはAfterEffectsからTimelineのassetに変換するものは作りたい。

それがあればAEを多少いじってもプログラマが一切知らないまま

ABが更新されて勝手に出る、という状態にできる。

デザイナーが手持ちの実機で確認できるところまで

整備しておかねばならない。

非Active時にTransformをいじることの罠

長いこと謎の症状に見舞われていた。

 

DOTweenで拡縮のアニメさせていた物をGameObject.SetActive(false)で消した後、

GameObject.SetActive(true)で復活させると、おかしな大きさになるのだ。

復活させた時はアニメはつけていない。ただ静止画として出しただけだ。

しかも、大きさは毎回同じなわけではない。微妙に大きかったり、

微妙に小さかったりする。

 

さらに、明らかに画面上では大きさが変にも関わらず、

Inspectorで見ると値は正しい。例えばどう見てもスケールが1.3くらい

ある絵なのに、1だったりする。

そこで、試しにInspectorで1.0001とかを入れてみると、

大きさが正しくなる。1を入れてもおかしなままで変化はない。

 

Inspectorの表示がおかしいのか?と思い、スクリプト

スケールの現在値をDebug.Logしてみるが、やはり1だ。正しい。

 

実害がさほどなかったので「謎現象」として放っていたのだが、

今日たまたま著しく困るケースがあったので調べてみて、

ようやく原因と対処がわかった。

 

 

Transformの何かをいじると、普通は即時反映される。

例えばlocalScaleをいじると、そこにImageがついていたり、

その子孫にImageがついていたりすれば、その大きさはそのフレームに変更される。

子孫の位置や拡大率の変更計算が行われ、

また、uGUIのImage等があれば頂点を再計算する。

 

しかし、SetActive(false)されたgameObjectの下にあるTransformをいじった場合、

これらの計算が行われないようだ。

見えないのでわからないが、子孫要素の位置や拡大率は変化せず、

頂点もそのままにされる。

とはいえ、SetActive(true)された時にこれらの計算が行われて

正しい絵になってくれれば問題ないわけで、Activeでない時に計算を省くことは

なんら問題ではない。厄介なのは、これにもう一つのおせっかいが加わるからだ。

どうやらTransformの要素に同じ値を入れた時にはスルーする、

という実装になっているようで、これがマズいのである。

非Active状態で何か値を入れ、Activeにしてから同じ値を入れると、

非Activeで入れた値に関する計算は行われず元の絵のままなのに、

Activeにしてから入れた値がスルーされるので元の絵のまま、

という困った状況になる。

 

今回の場合、推測ではあるが例えば以下のような動作と思われる。

 

- DOTweenがスケールを1.3にする。

- SetActive(false)で消す。 

- 次に備えてスケールを1にしておくが、Activeでないので諸々の計算が省略される。

- しばらくしてSetActive(true)して、絵を初期化すべくスケールを1にする。だが、「すでに1が入っている」ので、計算がスルーされる。

- 結果、スケール1.3の絵が出る。

 

今のところこう解釈すると矛盾がない。

実際「一旦スケール2を入れて、次の行で1を入れる」ことで回避できた。

おそらく諸々の計算が2回走って重くなるので、

「SetActiveする前に1を入れる」が最良なのだろうが、

コードの都合上面倒くさいので今回は「一旦2を入れる」で行く。

(SetActiveするgameObjectがアニメするgameObjectのかなり上流にあるので)。

毎度大きさが違ったのも、Activeだった最後のフレームにおけるDoTweenの

値がdeltaTimeの具合によって異なるからで、説明がつく。

 

Unityのバージョンは2017.1.2p3。5.6系の時代にもこの症状は出ていたし、おそらくはさらに前からだろう。

 

こんな根幹の挙動が今更バグるとも思えないので、たぶん仕様なのだろう。

辛い。

アセットバンドルの使い方で悩む

ダウンロード待ちは嫌なものだ。

 

極力少量のダウンロードでゲームを始められるようにしたいし、

まとめてダウンロードしてほしいタイミングでは

極力高速にダウンロードを済ませてほしい。

 

それには、元々のデータが小さいのが理想だ。

ダウンロードも速いし、管理の負担も小さく、

そもそも必要もないものを作らなかったということで開発も安く済む。

技術的には何の工夫もいらない。そうあるべきだ!

しかし、それで済むような小さなゲームでないのなら、

何かしら工夫する必要がある。

 

Unityで起動後ダウンロードする手段といえば、アセットバンドルだ。

サーバに置いておいて、初回はネットで送ってクライアントに保存し、

以降はローカルからロードする。

スマホの場合昔のゲーム機と違って光ディスクのように

遅いわけではないので、ローカルにあればそこそこ

高速にロードできるだろう。

光ディスクはファイルのロード開始までに100ミリ秒

とかかかる悪夢のような機械だったし、HDDですら

10ミリ秒とかかるのでファイルが多いと遅かった。

まあスマホが実際に速いかは測ってはいないのだが。

モノにもよるだろうし。

 

さて、ダウンロードを速くするには、ファイル数は少ない方がいい。

ローカルからのロードでもそうなのだが、ファイルが多いと「ファイルごとに

かかるオーバーヘッド」の影響を強く受ける。

ましてネットならなおさらだ。

「このファイルくれよ」「はいどうぞ」というやりとりをサーバとするだけで

0.5秒とかいう時間がかかってしまう。

キロバイトの小さなファイルごとにそれをやるのは馬鹿馬鹿しい。

そういうわけで、ダウンロードのことだけ考えれば、

巨大なアセットバンドルが一個あるのが一番速いわけだ。

 

ところが一方、更新の問題がある。100MBのアセットバンドルの中身の

テクスチャを1枚だけ更新したとする。お客はまた100MBダウンロード

せねばならない。これはマズい。更新が発生しそうな所に関しては、

バラでダウンロードできる方が良い。ただしダウンロードそのものは遅くなる。

 

結局のところ、それらのバランスを取ってまとめる単位を決めることになり、

それには「代表的なケースでの実際の数字」がないと話にならない。

例えばファイルごとに余計にかかる時間が1ファイル1秒だとしよう。

また、転送速度は1MB/sだとする。

総量が1GBであれば、1ファイルなら1000秒かかる。

10ファイルに増えても、1000秒+10秒で、あんまり変わらない。

それなら更新のことも考えて10ファイルの方が良さそうだ。

そして100ファイルになると、1000秒+100秒で、ちょっと遅くなってくるが、

管理上それくらいに割りたいのであれば、まだ許容できるかもしれない。

ただ、1000ファイルともなると2000秒になり、それはちょっと辛いので、

ファイル数はそんなには増やせないな、という話になる。

実際にはダウンロードを並列させることでファイルごとのオーバーヘッドは

軽減できるのでそのへんも見ないといけないし、

コンテンツ追加や修正の頻度や量も加味する必要があるだろう。

 

さて、落とした後のことも考えないといけない。

 

落とした後はローカルからロードするのだが、もし、

「全部読み込まないと中身が取れない」とすると、

あまり大きな単位でまとめるとマズいことになる。

 

例えば、1GBに1000個の画像が入っているアセットバンドルがあって、

そのうちの1枚の画像が欲しいとしよう。

この時に、1GB読まないといけない状態では困る。

最初の画像を出すための待ち時間がえらいことになるし、

メモリ消費も大変なことになる。

もし音を入れておいた場合、鳴らしたくなってからロードしたのでは

間に合わないだろう。ボタンを押した時の音があまり遅れると辛いことになる。

 

アセットバンドルの場合、LZMAで圧縮しているとこういう状況に陥るようだ。

ビルド時に何も指定しないとそうなる。総容量は減るが、展開が遅いし、

部分だけ取り出すことができない。

もしダウンロード速度を最優先してLZMAにするのであれば、

アセットバンドルはあまり大きくしない方がいいし、

明らかに同時に使うとわかっているもの以外はまとめない方がいいのだろう。

試しに1GBのテキストアセットを入れたアセットバンドルにLoadFromFileを

かけた所、60秒もかかった。どう考えてもファイル全体を処理している。

 

一方、LZ4、つまりChunkBasedCompression入りで

アセットバンドルを作っていれば、

部分だけ取り出すことができるようだ。LoadFromFile(Async)では

目次だけを読み込んでファイル全体は読まず、

LoadAsset~で初めて実際にファイルから読み込むように見える。

であれば、どんなにアセットバンドルが大きくても問題ない。

仮に何もかもが1つのアセットバンドルに入っていたとしても、

その時に使うものだけを読み込める。ロード時間もメモリも無駄にならない。

同じく1GBのテキストアセットを入れたアセットバンドルを作ってみたが、

LZ4をかけておけば1秒でLoadFromFileが終わる。

ファイル全体を見ていたらありえない速度だ。

 

ゲームのデザインや素材の物量によっては、

「とにかくたくさんあって、いつどれが使われるか事前に予測し難い」

という状況はありうる。

例えばエフェクト類とか、短い音声とかだ。

操作や展開次第で「何を鳴らすか」「何を出すか」が違ってくる場合、

使い得る物を全部メモリに展開しておきたくはない。

使わずに終わるデータがあるなら無駄なロードをしたことになるし、

そもそも同時に全部メモリに置くことが許容できないかもしれない。

遅延が許容できる範囲で済むなら、使うとわかってからロードしたいわけだ。

しかし、ファイルがバラだとダウンロード時間が長くなる。

キロバイト量のファイルが数千個、みたいな状態は嫌だ。

もしそういう時に、「1ファイルに数千個入れておけるが、

ロードの時にはその中の必要なものだけを取り出せる」のであれば、

全部まとめて1ファイルにしても問題が出ない。

LZ4のアセットバンドルはそれを可能にしてくれるものと思われる。

昔はそれを可能にするためにファイルを結合するものを自作していたし、

「サーバにはzipして配置、クライアントで展開してアセットバンドルをバラで配置」

といった手も考えられるが、その必要はなさそうだ。

 

さて、もう一つ考えておかねばならないことがある。

「どのアセットがどのファイルに入っているかを誰が知るか」だ。

 

コードを書いている時に考えることは、

「charaAという名前のSpriteが欲しい」

とか、

「bgm_battleという名前のAudioClipが欲しい」

というようなことで、それがどのファイルに入っているかなんて

知りたくもない。

 

ファイルにまとめる単位は上記のように諸々の事情で決まるため、

コードを書いた後でまとめることだってあるだろう。

更新のことを考えて、まとめ方が後から変わることだってある。

であれば、コード中にファイル名は一切出て来ない方がいい。

アセット名だけで済むように仕掛けを作っておく必要がある。

 

しかし、当然ロードするにはファイル名がわからないといけない。

「○○というファイルの中の、××というアセット」と指定しないと

ロードできないからだ。

そこで、「××というアセットは○○というファイルにあるよ」

ということを知っているクラスをどこかに用意して、

そのクラスに「○○くれよ」と言うと、それが入っているファイルを

開けて出してくれる、という感じにする必要がある。

 

AssetBundleの場合、作成時にmanifestなるものが吐かれ、

そこには「何というアセットが入っているか」が書かれている。

これを使うのが良さそうだ。

加工してjsonなりにしてサーバから送信してもいいし、

これをそのままクライアントにダウンロードしてしまって、

落としてから加工して目録を作ってもいいだろう。

そのへんはやったことがないので何とも言えない。

しかし何かしらそういうものが必要になる。

 

ただ、ファイル名を指定したいこともある、というのは注意がいる。

例えばキャラごとにファイルがあって、

それぞれにfaceとかbodyとかいうスプライトが入っている、みたいな時だ。

読むファイルだけ換えて、後のコードは一緒にしたい。

virtualな関数みたいなものである。

なので、ファイル名の指定をできなくしてはいけない。

そもそもアセット名がグローバルに唯一、なんてことは保証し難いわけで、

同じ名前のアセットが複数あるケースはあるものと考えないといけない。

アセットの名前だけでロード要求された場合、それが複数あったらエラーを返す、

というようなことになるだろう。「最初に見つかった奴を返す」

とかだと絶対バグるだろうな。

 

以上まとめると、

 

- アセットバンドルのファイル数が少ないほどダウンロードは速いが、更新や管理と妥協しつつ作る単位を決める感じになるんだろう。

- LZ4(ChunkBasedCompression)でアセットバンドルを作れば一部だけ取り出せるので、同時にロードする確証がないものでもまとめて入れておける(っぽい)。

- 何がどのファイルに入ってるかを気にしないでロードできるようにしたい。

 

という感じだろうか。「一部だけ取り出し」が本当にそのように動作するかは、

実のところまだ確認してない。

1万ファイルくらい入れた巨大アセットバンドルから1個だけ取り出して、

プロファイラで測定する、ということを後でやっておく。

さすがに確認しないと怖くて使えない。

後でここに追記するか、別記事で書くと思う。

テクスチャ圧縮の厄介さ

Unity、というか、スマホ向けにゲームを作るにあたって、

テクスチャ圧縮は悩ましい問題だ。

 

ゲーセン及び家庭用では「とりあえずBC3(DXT5)」

な時代が長かったし、BC4やBC5、A8やL8といった

フォーマットも選択肢としてあった。

対応機種が複数であっても、

手元に全ての機械があったし、仕様もはっきりしていた。

圧縮は自前でやっていたので、

デザイナーが自力でやる、サーバに置いて自動でやる、

みたいな選択肢も自分側にあった。

BC7が現れて圧縮に時間がかかるようになった時にも、

マシン並列やGPU化で高速化する選択肢があった。

 

だがUnityでスマホ向けとなると話が違う。

 

まず、機種が全部手元にない。比率も確実にはわからず、

本当に動くかもわからないとなると、

安全側に振らざるを得ない。

いろんな形式を用意して、機種によって最適なものを

使ってもらう、というのも手間をかけられるなら良かろうが、

手間イコール金と時間なのが難しい。

そういうわけで、「全機種で使えるであろう形式」

に絞れると楽、という事情がある。

あるいは古い機械を相手にしない、という選択もありうるが、

それは技術というよりは商売の問題だろう。

 

もう一つ面倒なのは圧縮がUnityの中で行われ、

その間は操作不能になることだ。

また、複数バージョン持っておくこともできず、

設定を変えれば古いものは消えてしまう。

とにかく圧縮のやり方に関して自由度がない。

別のマシンで並列化とか、自作の高速仮圧縮で

とりあえず体裁だけ整えるとか、

低画質版と高画質版を両方持っておくとか、

そういうことができない。

なおさら、全部の機械で動くと期待できる形式だけを

用意して終わりにしたくなる。

 

Unityで使える形式は、まず、無圧縮32bit。24bitとあっても、

メモリの中では32bit食う機械も多かろう。

そういう機械では24bitでロードすると32bitへの変換が

走って余計に遅くなる。

無圧縮と言えば32bit、と考えて良いのではないだろうか。

 

16bit系は565っぽいもの(RGB16)と、4444っぽいもの(RGBA16)。

5551、つまり色15bitにアルファ1bitのフォーマットは

使えないように見える。

 

8bitの1チャネル系はA8だけ。

シェーダでどう出てくるかは確認していないが、

RGBが1でアルファだけ値が出てくるとすると、

若干使いにくい。

 

もっと減る形式として古い機械でも動くものは、

Android用にETC1、iOS用にPVRTC。

ETC1は透明度を持てず、

ETC1、PVRTC共にかなり劣化の仕方にクセがあって、

苦手とする画像が異なる。

余計な手間を減らそうと思うと、

Androidではこの画像が汚ないのでどうにかしたい」

みたいなことは極力避けたい。

そもそも実機で確認する、ということ自体が手間なので、

UnityEditor上で劣化の状況まで確認できる方が良いのだ。

とはいえ1画素4bitという圧縮率は捨てるには惜しいので、

ほどよい匙加減が求められる。そのへんが面倒くさい。

 

両方で同じものが動くようになるのは、

ASTCの普及後ということだろう。

人によっては「すでに普及している」と考える人もいるだろうが、

それは商売の問題だ。

個人的には私が持っているスマホが対応していないので、

私は「ASTCはまだ普及してない」と思っている。

 

 

さて、そんな面倒くさいテクスチャ圧縮を

何のためにやるかと言えば、

 

- 実行時のメモリ使用量の削減、

- 実行速度の向上

 

この二つだ。ストレージ容量や、ネットでの通信容量

に関しては、pngやjpgにした方が余程減るわけで、

テクスチャ形式なんて気にする必要はない。

しかしpngやjpgは展開しない限り描画に使えないわけで、

ブラウザだってメモリの中ではjpgやpngは展開されている。

なのでメモリ消費量を減らそうと思えばjpgやpng

役に立たない。GPUがハードウェアとして

対応している形式でなければどうにもならない。

 

となると、jpgなりpngなりで転送、

ストレージに置いておいて、

メモリにロードしてからGPU向けの圧縮形式に変換して使う、

というのが理想なはずなのだが、そうは行かない事情がある。

このGPU向けの圧縮形式への圧縮に、

凄まじく時間がかかるからだ。

とても実行時にできるような代物ではない。

ただし、よほど品質を妥協すれば、

jpgで読んで圧縮し直して使う、ということは実際可能だ。

Unityでもやってできないことはないように見える。

ただ凄まじく面倒くさいことになるし、

品質も論外になるため、

かなり限られた用途でしかやらないだろう。

例えば、莫大にたくさんある画像から実行時に何かしら合成して、

しかもそれを多量にメモリに載せないといけない、とかだろうか。

例えばスポーツ選手が数千人入ったゲーム、

みたいなものだと、顔写真データは極力減らさないと

転送が遅くなりすぎるし、ストレージも辛い。

なのでjpgで持っておいて、ロードしてから展開したくなる。

しかし同時に多数の選手が出てくるなら、

無圧縮だとメモリの消費も痛いだろう。

後述するように性能の問題もある。

そこで品質に妥協してでも圧縮をかけ直す、という選択は

ありえる。実際にやる例があるかどうかは知らないが。

 

そして性能にも影響する。テクスチャをメモリから画像データを

演算器(シェーダ)に運んで、計算し、

それをまたメモリに書き出すのが描画というプロセスなわけだが、

最初にテクスチャをメモリから運ぶところは量が多いほど

時間がかかる。電気も食う。

さらに、演算器から見ればメモリはずいぶんと遠くにあるため、

演算器の近くに小さなメモリ(キャッシュ)を置いておいて、

良く使うものはそこに置いておくようにしているのだが、

データが大きいとすぐにそれが一杯になって、

遠くのメモリまで見に行かねばならなくなる(キャッシュミス)。

結果、テクスチャがデカいと性能も落ち、電気も食う。

 

しかし、これに関しては実際どれくらい遅くなるかは

機械による。そしてその機械が全て手元にあるわけではない。

手元の機械で測っても、違う挙動をする機械はありうる。

メモリが高性能でデカいテクスチャでもビクともしない

こともあるだろうが、安物はそうは行くまい。

実際、メモリと演算器をつなぐ道幅を広くすると

テキメンに機械の値段が上がるので、

安物は道幅を狭くしてあることが多い。

iPhoneは基本高級品なので、iPhoneでばかり見ていると

私のようにymobileで「安いのくれ」と言って買うような

タイプの人を見逃すことになる。

スマホが出始めの頃は毎年買い換えるようなタイプの人が

多数いて、それに応えるように

毎年性能が倍々になっていく世界だったが、

今はもはやそうでもない。

「頑丈で防水」とか「デザインがいい」とか、

そういう理由で買われる機械の性能を高める理由はあまりない。

実際、私が2017年後半に買ったS2という機械は、

2012年発売のiPhone5にも大きく劣る。

「新しい機械は速い」はもはや成り立たないのだ。

要求性能を低くできるなら、それに越したことはないだろう。

テクスチャ圧縮はそのための武器になる。

 

ただ、逆に言えば、メモリ消費量と性能の問題がないなら、

こんなに面倒なテクスチャ圧縮なんてしなくてもいい、

ということではある。テクスチャ圧縮なしで問題ない

サイズや性能要求に収めて開発する、という選択はアリで、

たぶんそれはコストを結構大きく下げる。

「メモリ4倍だぞ?圧縮しないとかありえんだろ」

と私なんかはすぐ思ってしまうが、それが持ちこむ

面倒の大きさは結構バカにならない。

ましてUnityの場合それを解決する手段がなく、

インポート時間やビルド時間の増加はボディブローのように

ゲーム開発を蝕むのだ。jenkinsに出したから大丈夫、

などという単純な話ではない。

圧縮済みのテクスチャをインポートできたらいいのにと

本気で思うが、今できないということは

そういう要望はあんまりないということなんだろうな。

高速化なあ...

技術ブログらしく、ちゃんと構成を考えて、

いらんことを書かずに情報価値の高いものを書こう、

と思っていたのだが、その結果、一度も書かないまま

数ヶ月が過ぎるという状態になった。

 

本を書いてすら脱線と雑談まみれのものになるのだから、

本ほどの労力をかけられるはずもないブログが

まともなものになるわけがない。

 

あきらめた。それよりはメモでも何でも書いた方がいい。

そもそも、情報それ自体で勝負するスタイルでは私は不利なのだ。

 

さて、最近は高速化をやっている。もちろんUnityでの話だ。

 

過去、高速化仕事をせずに終われたプロジェクトはなかった。

しかしUnityならば違った展開もあるだろう、と期待していた。

エンジンの思想に沿った範囲で無理なく作り、

それで浮いたコストをより価値のある、お客の喜ぶところに

つっこむ、というのが皆が幸せになる道であるはずだ。

スマホの処理速度はおおむねPS VITAを越えており、

あまり無理をして高速化せずとも十分綺麗なものができる

公算は高い。

 

でもまあ、やっぱりそうは行かないよなあ。

 

速いマシンはより遅いコードを書くために使われる。

UnityのオーバーヘッドとC#のオーバーヘッド、

それに数々の新しい書き方のオーバーヘッドが

重なると、とてもVITAより速い機械とは思えない状況になる。

 

では、Unityを2Dメインで使っている場合の負荷はどこに来るか。

 

- シーンの初期化スパイク

- 動的Instantiateによるスパイク

- ガベコレによるスパイク

- DrawCall関連負荷

- 通信関連負荷

- 2Dゲーム特有の重ね塗りの激しさ

- Canvasの頂点詰め直し関連負荷

 

このあたりだろう。

 

シーンの初期化は悩ましい問題だ。

そんなの別スレッドで初期化まで済ませてくれるんでしょ?

くらいに思っていたが、ユーザスクリプトは基本メインスレッド

でしか動かないわけで、AwakeやらOnEnableが

大量についているとメインスレッドが遅くならざるを得ない。

昔ながらのやり方で、画面が止まってる時にガッツリ

初期化してしまうのがたぶん一番いいのだろうし、

Unityはそういう前提で作られているように見える。

 

しかし、それでは待ち時間とメモリ消費が増える。

多少ゲームがガタついても、必要になるまで初期化を遅らせれば、

最初に画面が出るまでの待ち時間は減るし、

そもそも使わずに済むものを無駄に作ることもなくなるから、

合計の待ち時間は減るのだ。

スパイクを削るために、エフェクト全種類を最大数生成する、

みたいなアプローチは、必ずしも良いとは言い難い。

メモリの消費量の問題もある。

 

なので、シーンの中にはあまりモノを置かず、

極力プレハブから動的初期化、ということにしたいのだが、

これがまた重い。作りやすさを重視すると、

行列乗算の必要がなくても「単にまとめるためにgameObject置く」

みたいなことになりがちで、どうしても無駄に重くなる。

匙加減としか言いようがない。

 

ガベコレも面倒だ。ガベコレは完全には抑制できないし、

C#的な普通の書き方をしていれば毎フレームキロバイト

量のnewがされるのはやむをえない。

だいたい通信が来れば、そのデシリアライズ

避け難くnewが走るわけで、エフェクトの類をいくら

事前生成+使い回ししてもゼロにはできない。

foreachも配列以外では全てnewが走る。

となれば「気にしたら負け」くらいに思いたいところなのだが、

さすがに限度がある。数十ms止まる、みたいな状態は

さすがに許容し難い。ガベコレの負荷は使用するメモリブロックの

数に比例するはずなので、とにかく数を減らしたいのだが、

なにせnewは見えにくい。boxingはとりわけ厄介で、

デシリアライザの類でやむをえずobjectを経由する、

みたいなのはなかなかに避け難い。

 

DrawCallも重い。よくSetPassが問題でDrawCallは

問題ではないというようなことが言われるが、

GLES2でもそうなのかは確認していない。

DX11やGL4はスマホからは遠い世界であり、

PC向けインディーを作っている人達の知見が

そのまま使えるとは限らない。

GLES2はよほどうまく実装してもそう速くできないだろう。

ドライバ層が厚すぎる。

metalやGLES3、vulkanなどもあるが、

そういうAPIが使えるのは高い機械で、

高い機械は放っておいても速いのだ。

「いい機械を持っている人にはいい体験をしてほしい」

という気持ちはあるし、やれる範囲ではやるが、

手元にない機種に多数対応せねばならない状況では、

できるだけ同じプログラムを通したい。

GLES2を捨てられないなら全部GLES2の方が

問題は起こりにくかろう。

 

通信関連負荷も馬鹿にならない。

文字列処理は元来重い処理で、ゲーム中に動的にやるのは

避けたいものだ。しかし、作りやすさを考えると、

文字列は扱いやすく、仕様変更にも強い。

そうなると、JSON的なものが中核に入ってくる。

しかしこれがまた重い。まして自動でクラスインスタンス

生成する現代的な形でのデシリアライズをやると、

リクレクションまみれのコードになる。

データ設計が現代的だと、クラスが多数ネストするので、

newも増える。

こういう状況で、通信を無視できる負荷

に抑えるのは結構しんどい。

「無視できる負荷」というのは、

想定する一番ショボい機械で1ms未満だ。

今のところの感覚として、エディタの3倍くらい

は時間がかかる感じなので、エディタで0.3ms

に収まっていればいいのだろうが。

 

あとは2Dゲームの重ね塗りのエグさも如何ともし難い。

画面を暗くするのに上に黒い透けた板を重ねれば、

それで一画面プラスだ。

不透明なものがほとんどないので、奥から描かざるを得ず、

Zで削ることもできない。

画像を不透明部と半透明部に割って先に不透明部を

手前から順にZテスト付きで描く、

という最適化は理屈上可能だが、

それを内部で自動でやるように仕組みを作るのは至難だ。

内部自動でできなければ開発コストが増すわけで、

ただでもかさむUI実装負荷を増すようなマネはできない。

UIは仕様変更が多く、組む手間が大きくコストに響く。

極力「Unity的にフツーの組み方」そのままでやれないとキツい。

かといって謎のスクリプトで最適化をかけてからビルド、

のようなアプローチは、「ビルドした時だけ絵が変なんだけど」

みたいなバグの原因になって、それはそれでコストになる。

 

しかも、スマホの機械はおおよそメモリ帯域が狭いだろうから、

なんぼシェーダが単純でもテクスチャフェッチと塗り

でごっそり持っていくだろう。

透明な部分をポリゴンを切ってくれれば軽減はできるが、

標準のUI.Imageはそんなことはしてくれない。

透明部を切り落とすものを自作することはできるが、

それで頂点が増えればCanvasの詰め直し負荷が上がる。

 

canvasの詰め直しの負荷は結構深刻で、

頂点数が数百でも無視できない負荷になる。

とりわけ、無駄にgameObjectの階層を重ねて

Transformが多数ある場合は重いっぽい。

それだけ行列演算のコストがかさんでくるということだろう。

 

動かないものと動くものを分けろ、と言うが、

そんな面倒くさいことはできればやりたくないし、

下手にやればDrawが増えて逆効果になりかねない。

その意味で、よほどはっきりと割れない限りは上策とは言い難い。

それに現代的なゲームでは大抵のものは動くのだ。

 

頂点数が増えがちなのは文字描画だ。

最低でも文字数×4の頂点が必要になり、

標準のOutlineをつけるとそれが7.5倍にふくれ上がる。

リッチテキスト有効だとタグ分も頂点も加わり、

それはもうひどいことになる。

面積ゼロの三角形を削るフィルタをかましてタグ

分は削れるが、その負荷もタダじゃない。

また品質が許容できれば軽量版のOutlineを用意したりするのだが、

それでも頂点が多い。

レンダーテクスチャに焼くなどせねばならないのだろう。

面倒なことだ。

 

ロクにまとめないままズラズラと書いてきたが、

本当、容易じゃないなあ。

技術ブログをやろうと思う

私は20年以上前から

http://www.page.sannet.ne.jp/hirasho/

に日記を書いてきたわけだが、内容はごちゃまぜで質もバラバラだ。

技術についてきちんと文章にまとめる機会がある方が腕が上がると

思っているので何か書きたいのだが、

転職してからというもの仕事が忙しいのでとても本など書いているヒマはない。

だがブログの形であれば小出しにできる。

そこでここにアカウントを作ってみた。技術者の間ではここがメジャーらしい。

 

さて、今の私に人様に伝えられるような技術があるのか?

なにせUnityでモノを作るようになってもうすぐ1年という素人だ。

 

しかし、最近はどうやら普通に仕事ができている気もするし、

UnityだろうがC#だろうがプログラムを書くことに変わりはなく、

C++でやっていたことはほとんどそのまま活きている気がする。

職場でも私以外があまりやらないことがあったりするので、

そういうことを書くのは誰かにとって面白いかもしれない。

 

さて今日は内容はないが、漠然と考えているネタを列挙しておく。

  • update+switch vs コルーチン
  • delegate vs 直接呼び出し
  • 動きをつけるのに使える「数学的な意味での」関数について(expとかlogとか)
  • IEnumeratorそしてコルーチンというのはつまり何なのか
  • 3DオブジェクトとUGUIを協調させる(3D空間内の物体をUIに向かって飛ばすとか)
  • よくある隠れたメモリ確保について
  • プログラムでタップを発生させてボタンを押しまくる話(EventSystemでのやり方)
  • 全てのcanvasについて、マテリアル数、頂点生成が走るか否か、その頂点数の概算、を得て性能解析に役立てる話。実装と用法。
  • 敢えておおまかに性能を測ることのすすめ。理屈と実践。
  • AfterEffectsからUnityにアニメを持っていく話

たぶん他にもいろいろある気がするし、実際問題いつ書けるのかわからないが、

今日のところはともかくも「ブログを始めた」ということで。