フューチャー技術ブログ

UnityのShaderでVRプラネタリウムを作ったけどうまくいかず悔しかったので自作の星を作ったら宇宙が終わった

はじめに

こんにちは。TIG DX ユニット所属、金欠コンサルタントの藤井です。

夏の自由研究連載の第2弾です。夏休みの宿題っぽいものを題材にしてみました。

実は私、星を見るのが好きなので、天体望遠鏡を買ったり、星を撮るためにカメラを買ったりしています。せっかくならいい環境で見たいので、南は沖縄から北は北海道まで星を見に行っています(海外も行きたい)。ただ、星を見に行くとなるとどうしても長距離移動になるので、お金もかかるし大変です。天気に左右されるのもつらいところです。

ということで今回は、遠出しなくてもいろんな星を見ることができるように、VR でプラネタリウムを作ろうと思います。

なお、本記事では Unity そのものの機能や、Shader の基本的な部分には触れず、作成したソースを参照しながら、重要な部分のみを詳細に記載しています。

事前準備等

大方針

VR プラネタリウムを作るためには、VR 空間上で星の配置を再現していく必要があります。実現方針としては概ね以下の 2 つに分かれるかと思います。

  1. 実際に宇宙空間を作成し、星をオブジェクトとして配置していく
    • メリット:非常にわかりやすく、実装コストが低い。
    • デメリット:大量のオブジェクトを作成することになるため、負荷が高い。
  2. 天球を作成し、天球上に星の絵(テクスチャ)を描く
    • メリット:オブジェクトは天球のみのため、負荷が低い。
    • デメリット:テクスチャの解像度に依存して厳密性が低下する。

ところで皆さんは宇宙にどれぐらいの星(恒星)があるかご存知でしょうか。星の数ほど、というぐらいなので途方もない数であるということはイメージできると思います。結論としては、宇宙の大きさが無限大なので、星の数も無限大です。とはいえ、我々が地球から観測できる星の数は有限であるため、十分な観測技術があれば星の数を数えることは原理上は可能なはずです(現在確認可能な範囲でも数十億個はあると言われています)。

この観測できる星をすべて網羅した、星表と呼ばれるものを作りたい、という野望を天文学者は古より抱いており、紀元前から現代に至るまで様々な星表が作られてきました。ただ、これらの星表はもちろん完璧なものではなく、実際に記載されているのは数十万程度です。

母数が多いので、数十万と聞くと少なく見えますが、決して小さい値ではありません。特に今回は Meta Quest 2 を用いてプラネタリウムを作ろうと思っているので、マシンスペックもそこまで高くありません。そのため今回は、負荷を低減できる方針 2 を採用します。

Shader で Skybox に動的に星を描いてみる

では、天球上に星を描いてみましょう。ただし今回は天球用のオブジェクトは作成せず、Skybox と呼ばれるものを使います。Unity にデフォルトで用意されている、オブジェクトが存在しなければ描画される背景のようなもので、ゲーム空間を取り囲むように配置されています。

Skybox に星の絵を描くに当たり、実際に星空のテクスチャを描いて貼り付けても良いのですが、今回は Shader で動的に星景を作成することにしてみました。理由は Shader を書いてみたかったからです。

Unity では、「ゲーム空間内の(x,y,z)という座標に hoge というオブジェクトが有るから、画面上の(X,Y)座標のピクセルには(r,g,b)という色を出力しよう」という処理を Shader と呼ばれるものが行っています。
つまり Shader に適切な指示を出してもらえば、実態としてはゲーム空間内に何も存在していないにも関わらず、画面上には何かが表示されている、ということができます。

これにより、単純にゲーム空間内に召喚するオブジェクトが減るという効果はもちろんあるのですが、それに加えて Unity では、C#で書かれたロジックは CPU で(ほぼ)直列に処理がされ、Shader は GPU で並列に処理がされます。基本的に描写する対象の数が多いほど、処理の並列度が重要になってくるため、星の表示はすべて Shader にまかせてしまえば、非常に軽快に宇宙を再現できるのではないか、という気がしてきました。
なお、結論から記載すると、これはダメでした。

Shader を書いてみる

Unity の Shader では、HLSL と呼ばれる言語を用いて処理を記述していきます。

とりあえず試しに 2 つ、白と赤の星が表示されるようなものを作ってみたものがこちらの sample1.shaderです。

重要なのは以下のあたりです。

// 星の存在する方向や色、明るさを定義
static const Star stars[2] = {
createStar(float3(1, 1, 0), fixed3(1, 1, 1), 0.5),
createStar(float3(0, 1, 1), fixed3(1, 0.5, 0.5), 1),
};

// 背景色を定義
fixed4 drawBackground()
{
return fixed4(0, 0, 0, 1);
}

// 星を描画
fixed4 drawStars(float3 dir)
{
float3 o = float3(0, 0, 0);
for (int i = 0; i < 2; i++)
{
float3 direction = normalize(stars[i].direction);
fixed3 color = normalize(stars[i].color);
float angle = dot(dir, direction);
o += color * pow(max(0.0, angle), _radiusCoefficient * (stars[i].magnitude + 2)) * pow(2.5, -1 * stars[i].magnitude) * _brightnessCoefficient;
}
return float4(o, 1);
}

// 各ピクセルごとの色を決定
fixed4 frag(v2f i) : SV_Target
{
return drawBackground() + drawStars(i.texcoord);
}

コメントで書いている通りなのですが、frag()という関数の返り値に応じて、画面上の各ピクセルごとの色が決定されます(#pragma fragment fragという記述で定義しています)

このfrag()の引数であるiのメンバー変数texcoordには、画面内の各ピクセルにおける視線方向のベクトルが格納されています。この視線方向のベクトルと、別途定義している星の存在する方向を示しているベクトルとの内積を取ることで、どのピクセルに星を描画するべきかを決定しているのがdrawStars()です。ざっくり書くと、視線方向と星の存在する方向が同じであれば星を描画し、若干ずれていたとしても、星の明るさに応じてほんの少しは描画する。みたいなことを表現しています。これを星の数だけ繰り返すようループしています。なお、shader では単純なループは適切に最適化され、並列処理が行われます。配列の要素間のやり取りなど、直列実行が不可避なものに関しては並列化されませんが、そもそもそういう処理は GPU で実施することを推奨されません。

さて、この Shader を Skybox に適用するとどのように星が見えるかというと、こんな感じです。

image.png

結構それっぽい気がしますね。

ではここからどんどん星を増やしていきましょう。と思ったのですが、以下のような制約の壁にぶつかり続け、結局は断念しました。

  • Shader では外部ファイルからデータを読み込んで描画できない。
    • そのため、星の情報をすべてハードコーディングすることになりました。
    • ファイル読み込みを C#で行い、Shader に結果を渡す、みたいなこともできる気がしたのですが、うまく行かず…
  • Shader で利用できる配列の要素数に上限があった。
    • 1 配列の要素数、ではなく、全配列の要素数の合計が 4096 以下である必要があるようでした。
    • そのため、配列ではなく個別に変数を大量に宣言することになりました。
  • 10 万個の星を描こうとしたところ、Shader のコンパイルがタイムアウトした。
    • 個別変数の数に応じてコンパイル時間が伸びていき、やがてはタイムアウトしてしまいました。
    • 環境変数でタイムアウト時間が設定されているようなので、伸ばすことは可能そうなのですが、とりあえずは星の数を減らして試してみることにしました。
  • 1 万個ぐらいの星を描いたあたりで、PC が唸りを上げ始めた。
    • GPU がフル回転を始めました。
    • あと普通にめちゃくちゃ重たくなっていたため、ここで心が折れました。

一応ある程度は描けていたので供養のためキャプチャを掲載しておきます。

なんか、星空って感じではないですね?

もしかしたら色や明るさを計算するロジックにバグがあるのかもしれませんが、心が折れたのでこれ以上は深追いしません。

テクスチャを描いて貼り付ける方針に切り替える

Skybox に動的に星を描く方針は頓挫したし、とりあえず Shader を書いてみるという目的は達成したため、素直にテクスチャを描いて、それを貼り付ける方針に切り替えます。切り替えは大事。

テクスチャを描く

まずはテクスチャを作成するための星のデータを用意します。

今回 VR プラネタリウムを作るにあたって、とりあえずは実際の星空を再現したいので、観測データを用いることにします。冒頭で星表というものを紹介しましたが、今回はその中でも、ヒッパルコス星表というものを使っていきます。ヒッパルコス衛星により取得されたデータを編纂したもので、12 万弱の星のデータが記録されています。NASA がデータを公開してくれているので、こちらから必要なデータを取得していきます。

HIP 番号(name)、赤経(ra)、赤緯(dec)、等級(vmag)、B-V 色指数(bv_color)を選択してダウンロードしました。そのままだと使いづらいので、以下コマンドなどで csv に変換しておきます。

cat hip.txt | sed '1,5d' | sed 's/ \+|/|/g' | sed 's/| \+/|/g' | awk -F '|' -v 'OFS=,' '{print $2,$3,$4,$5,$6}' > hip.csv

この csv をもとにテクスチャを描画するpython スクリプトを作成しました。

ざっくりこんな感じのことをしています。

  • 赤経・赤緯を度(degree)に変換して描画する位置を決定する。
  • 等級と赤緯から画像上での星の大きさを決定する。
    • 本来星はものすごく遠いため、明るさによらず点として見えるはずですが、人の目の錯覚として明るいものは大きく見えたり、大気による散乱効果が明るいほど強いことなどを踏まえ、明るい星ほど大きく描画しています。
    • 天球を平面のテクスチャで表現するため、赤道部分はそのままに、極地に近づくほど横に引き伸ばした描画とする必要があります。
  • 等級・B-V 色指数から各星の色・明るさを決定する。
    • 観測値である B-V 色指数から星の表面温度を推定、黒体を仮定して表面温度から RGB 値を算出しています。
    • 等級が 1 変わると、2.5 倍明るさが変わるので、HSV 色空間における V 値が等級に応じて増減するようにしています。
      • ただし、人の目が明るさ(暗さ)に慣れたり、カメラの露光時間を長くしたりすることで、絶対的な明るさと見かけの明るさは必ずしも一致しないことを考慮し、可変パラメータとしています。
    • 計算の根拠はスクリプト上に参考 URL を記載しています。
  • ガンマ補正を行う。
    • 人間の目やカメラによる補正として、ガンマ補正ができるようにしています。こちらもパラメータは可変です。

スクリプトにより描画したテクスチャがこちらです。

高解像度画像に、小さな星の点を表示しているので、あまりきらびやかではないです。

テクスチャを Skybox に適用する

テクスチャが生成できればあとは Unity に取り込んで、適切に設定するだけです。

まずは Import して、解像度を高めに設定しておきます。

image.png

そしたら Skybox 用のマテリアルを作成し、Shader をSkybox/Panoramicに、Mapping をLatitude Longitude Layoutに設定し、テクスチャのところにインポートしたテクスチャを適用します。

image.png

最後にメニューの Window -> Rendering > Lighting から Lighting 設定を開き、Environment タブにある Skybox Material に作成した Material をセットします。

これで準備は完了です。VR 環境で見てみましょう。

output.gif

うーん…とりあえず、星空っぽくはある…?(なんか、オリオン座の形おかしい気がする)。

おわりに

ここまでやっておいてなんなんですが、思ったより微妙でした。
考えられる要因はいくつか有ります。

  • 解像度が足りていない。
    • テクスチャの解像度・VR ヘッドセットの解像度ともに、現実世界には遠く及びません。そのためどうしても星というよりは、なんか光っている点、みたいに見えてしまいます。
  • 星の数が少ない。
    • 10 万強の星を再現しているとはいえ、当然宇宙に存在するすべての星を再現できているわけではありません。
    • 特に、天の川銀河の外にある銀河の星や、星雲などのデータはヒッパルコス星表には含まれていないため、どうしても実際の星空とは異なってしまいます。
    • この問題を解決するには、実際に世界各地で撮った星景写真をつなぎ合わせてテクスチャを作るしかないかなあと思います。
  • 臨場感がない。
    • これは現実のプラネタリウムにも言えることですが、やはり現地で自然に囲まれながら見る星空、というものに、言語化できない魅力があります。気温や風、音など、単に視覚情報に限らない様々な情報が多角的に私達を魅了しているのだと実感しました。
    • 現実のプラネタリウムでは、この問題を、魅力的なナレーションや上映内容によって補っているのだと思います。
    • ゲームの背景など、脇役に据えるのであれば、このぐらいでも良いのかもしれません。

頑張った割にはこんなものか、というオチになってしまい悔しいですが、ともあれある程度のプラネタリウムを再現できたので、夏の自由研究としては良しとします。
ちなみに、今回はヒッパルコス星表のデータを使用しましたが、同じフォーマットでデータを追加することで、自由に星を追加できます。

・・・・・・・・

image.png

せっかくなので-75 等星である、「スーパー明るい藤井星」を追加してみました。見てみましょう。

output2.gif

宇宙は、なんか亀裂入ったし光に包まれて終わりました。