太郎Work

Unityとかで困ったこと等を残しておきます

VFXGraphで星空を描画してみた

はじめに

本記事は QualiArts Advent Calender 2019 24日目の記事になります。
昨日は @ogrszk の 「Spineでできること〜エディタのTipsとAPI使用例〜」でした


f:id:tarowork:20191121212257j:plain
星空
Unity2019になり、UniversalRenderPipelineなど新機能が追加されましたが、その中の一つであるVFXGraphを使用して星空を描画してみました

注意:

  • 天文学全くわからない素人なので厳密な計算とかは分からないです
  • 新機能の基本的な使用方法は割愛しています

環境

  • Unity2019.3.0b11
  • UniversalRP 7.1.5

目次

星のデータを用意

NASAに様々な観測データが用意されているのでそちらをお借りしました
https://heasarc.gsfc.nasa.gov/cgi-bin/W3Browse/w3catindex.pl#STAR%20CATALOG

PriorityがHIGHのものからデータ量の多い(約12万レコード) "Hipparcos Main Catalog" を使用しました

今回使用するのは等級 (vmag)、経度 (ra_deg)、緯度 (dec_deg)、色温度 (bv_color)なのでチェックを入れて出力します
※上記のサイトでは赤緯(dec)、赤経(ra)の代わりにパースしやすい(ra_dec, dec_deg)が用意されているのでそちらを使用しています

出力されたテキストファイルはこんな感じになります
後ほどUnity上で扱いやすいように変換を行います

Results from heasarc_hipparcos: Hipparcos Main Catalog
Coordinate system:  Equatorial
|vmag |ra_deg      |dec_deg     |bv_color|
| 7.84|149.16726051|-89.78245385|   0.097|
| 6.82|218.87831588|-89.77169600|   1.698|
...

プロジェクトの作成

作成時にUniversalRPのテンプレートが用意されているのでそちらを選びます。
f:id:tarowork:20191120190422p:plain

今回は大量の星を描画するためにVisualEffectも導入します
星は特に動かないので自前で生成するのもいいですが、新機能を使いたい+メッシュ制御面倒なので利用します。

f:id:tarowork:20191121114721p:plain
PackageManagerのVisual Effect Graphをインストール


星描画に使うPointCacheを生成

VisualEffectで座標を指定する方法は32bitテクスチャを直接使う方法もありますが、
PointCacheという仕組みを使用すれば内部で自動的にテクスチャ化してくれるのでそちらを利用します

f:id:tarowork:20191121142101p:plain
変換の流れ

テキストからMesh化するプログラム

ダウンロードしたテキストファイルは "|" で区切られているのでパースしながら任意の型に成形します
※StarCatalog内には一部情報が不足しているデータも存在しているので除外の必要あり

明るさ

明るさはvmagパラメータを利用します。
astro-dic.jp
人間の目は6,7等星程度しか見えないらしいので6等星の出力値を仮に1と置いてスケール値を計算します
m-n = -2.5log(l_m / l_n)

色はbv_colorパラメータを利用します
astro-dic.jp

このままではRGBとして扱えないため以下の変換を参考にしました
stackoverflow.com


上記の変換を踏まえソースコードを載せておきます 例外処理は無いです

public struct StarData
{
    public Vector3 position;
    public Color color;
    public float magnitude;
    public float intensity;
}

[MenuItem("Tools/Convert Star Mesh")]
public static void ConvertMesh()
{
    var path = EditorUtility.OpenFilePanel("Star Catalog", Application.dataPath, "");
    if (string.IsNullOrEmpty(path)) return;
    var meshPath = EditorUtility.SaveFilePanelInProject("Save Mesh", "StarMesh", "asset", "Save");
    if (string.IsNullOrEmpty(meshPath)) return;

    using (var reader = new System.IO.StreamReader(path))
    {
        bool initialized = false;
        var indexTable = new Dictionary<string, int>();
        List<string>[] data = null;
        while (reader.EndOfStream == false)
        {
            var str = reader.ReadLine();
            if (string.IsNullOrEmpty(str)) continue;

            // | が先頭に来るまでスキップ
            if (str[0] != '|') continue;
            var param = str.Split('|');
            if (initialized)
            {
                for (int i = 1; i < param.Length - 1; i++)
                {
                    var value = param[i].Trim();
                    data[i - 1].Add(value);
                }
            }
            else
            {
                // 初回
                // 両端は無視
                data = new List<string>[param.Length - 2];
                for (int i = 1; i < param.Length - 1; i++)
                {
                    var key = param[i].Trim();
                    indexTable.Add(key, i - 1);
                    data[i - 1] = new List<string>();
                }

                initialized = true;
            }
        }

        if (data == null) return;

        var vmagIndex = indexTable["vmag"];
        var ra_degIndex = indexTable["ra_deg"];
        var dec_degIndex = indexTable["dec_deg"];
        var bv_colorIndex = indexTable["bv_color"];
        List<StarData> stars = new List<StarData>(data[vmagIndex].Count);
        for (int i = 0; i < data[vmagIndex].Count; i++)
        {
            if (float.TryParse(data[ra_degIndex][i], out var ra_deg) &&
                float.TryParse(data[dec_degIndex][i], out var dec_deg) &&
                float.TryParse(data[vmagIndex][i], out var vmag) &&
                float.TryParse(data[bv_colorIndex][i], out var bv_color))
            {
                var intensity = Mathf.Pow(10f, (6f - vmag) / 2.5f);
                // 全パラメータ存在する
                stars.Add(new StarData
                {
                    position = Quaternion.Euler(dec_deg, ra_deg, 0f) * Vector3.forward,
                    magnitude = vmag,
                    intensity = intensity, // ソート用
                    color = bv2rgb(bv_color) * intensity // 最終的な色を計算
                });
            }
        }

        // 明るさ順にソート
        stars.Sort((x, y) => (y.intensity - x.intensity) > 0f ? 1 : -1);

        var mesh = new Mesh();
        mesh.name = Path.GetFileNameWithoutExtension(path);
        mesh.SetVertices(stars.Select(x => x.position).ToArray());
        mesh.SetColors(stars.Select(x => x.color).ToArray());
        AssetDatabase.CreateAsset(mesh, meshPath);
        AssetDatabase.SaveAssets();
    }
}

明るい星から順に並べ替えることで描画量を制御できるようにしておく

MeshからPointCacheAssetを作成

Window->VisualEffects->Utilities->PointCacheBakeTool
標準で作成するためのツールが用意されているのでこちらを利用

f:id:tarowork:20191121211219p:plain
PointCacheBakeTool設定

星のテクスチャを作成

f:id:tarowork:20191121211405p:plain
適当に丸を書いて保存

VFXGraph

VFXGraphはUnityが開発したエフェクトシステムで更新処理をComputeShaderに任せることにより、大量のパーティクルを高速に実行することが可能
モバイルでもComputeShader自体は動く端末が増えたが、動かない処理があったりするので慎重に使う必要あり
unity.com


Spawn

パーティクルを発生させるためにSpawn内にBlockを作成する必要がある
星は少しずつ出すのではなく最初に大量に1度だけ出したいのでSingleBurstBlockを使う
生成量を頂点数分(116812個)指定しているが1000等にすれば明るい星から順に生成される

f:id:tarowork:20191121214254p:plain
Spawn

PointCacheNode

先ほど作成したPointCacheAssetを指定するNode

f:id:tarowork:20191121213720p:plain
PointCacheNode

使い方は簡単で先程頂点と頂点カラーを出力しているのでそのまま

  • SetPosition from Map
  • SetColor from Map

のAttribute Mapに接続する

VFXGraphでは〜 from Map というNode名で様々なパラメータに適用することができる
今回はIndex0から順に描画したいのでSequentialを選択

AttributeMapをよく見るとテクスチャが表示されていて上から順に明るい色が格納されていることが確認できるので中身は普通のテクスチャだということが分かります

f:id:tarowork:20191121214930p:plain
PointCacheTexture

GetAttributeNode: color

GetAttributeNodeはパーティクル毎の各種数値を取得することができるNode
色の輝度に合わせてサイズを変えたほうが綺麗だったので適用
以下の画像のようにNodeを接続してSetSizeBlockに接続するだけ
f:id:tarowork:20191121215855p:plain

f:id:tarowork:20191121220901p:plain
パーティクルの近くに寄ると結構サイズが違うのが分かる

Output Particle Quad

ポリゴンとして出力するNode
作成したテクスチャを指定してカメラに向けるBlockだけ追加
f:id:tarowork:20191121220244p:plain

VFXGraph全体

特に動かしたりとかはしていないのでとてもシンプルになっています
f:id:tarowork:20191121221253p:plain

結果

全体像
f:id:tarowork:20191224104209p:plain

ポストエフェクトで輝度を上げたときの見た目
f:id:tarowork:20191224104251p:plain

レンダリング

  1. Unity Recorderを利用して8KでHDR撮影
  2. PhotoShopで少しだけ明るさ調整
  3. 縮小してjpg書き出し

f:id:tarowork:20191121222059p:plain
UnityRecorderはめちゃくちゃ便利なので綺麗に画を出したいときは必ず使ったほうがいいです
星が小さいため高解像で描画しないとブルームが綺麗に乗らないので8Kレンダリングしました
EXRファイルの容量が200MBもありましたが、、

まとめ

  • 極力プログラムを書かずに描画までうまくいったかと思います
  • 宇宙ヤバイ
  • 用語が全部専門すぎる
  • 正直ランダムに配置しても誰も気づかなそう