恒星データベースからUnity上に星空を構築する（リリースしました）

# 完成図

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/5afab459-8b5a-5b99-effc-c3d1be74e611.png)

　左から、

- 緑　春の大三角
- 赤　夏の大三角
- 青　冬のダイアモンド
- 紫　秋の四辺形

　
ゲーム画面として再生したやつは以下。

　WebGLアプリとしてリリースしました。
 
　https://hibit-at.github.io/StarSphere/

# 恒星データベース

　世の中には[恒星のデータベース](http://astronomy.webcrow.jp/)というものがあるらしいので、これをそのまま球体として配置すればリアルなプラネタリウムが出来るのでは？　と考えてやってみました。以下スクリプトのコード。

```C#
using System.Collections.Generic;
using UnityEngine;
using System.IO;

public class Reader : MonoBehaviour
{
    TextAsset csvFile;
    List<string[]> csvDatas = new List<string[]>();
    public Material[] _material;//このマテリアルは事前準備が必要

    //特定の星座に色をつけるためのリスト
    List<string> Spring = new List<string> { "Arcturus" , "Spica" , "Denebola" };
    List<string> Summer = new List<string> { "Deneb", "Altair", "Vega" };
    List<string> Autumn = new List<string> { "Markab", "Sheat", "Algenib" , "Alpheratz" };
    List<string> Winter = new List<string> { "Capella", "Aldebaran", "Rigel", "Pollux" ,"Procyon" , "Sirius" };

    int Color(string target)　//星座に対応した色をつけるための番号を返す関数
    {
        foreach (string c in Spring) if (target == c) return 1;
        foreach (string c in Summer) if (target == c) return 2;
        foreach (string c in Autumn) if (target == c) return 3;
        foreach (string c in Winter) if (target == c) return 4;
        return 0;
    }

    void Start()
    {
        csvFile = Resources.Load("mdata") as TextAsset; //ここで自前のデータを返す
        StringReader reader = new StringReader(csvFile.text);
        while (reader.Peek() != -1)
        {
            string line = reader.ReadLine();
            csvDatas.Add(line.Split(','));
        }

        foreach (string[] c in csvDatas)
        {
            int lon1 = int.Parse(c[3]);
            int lon2 = int.Parse(c[4]);
            float theta = lon1 * 15 + lon2 / 4;
            theta *= 2.0f * Mathf.PI / 360;
            int lat1 = int.Parse(c[7]);
            int lat2 = int.Parse(c[8]);
            float phi = lat1 + lat2 / 60;
            if (c[6] == "0") phi = -phi;
            phi *= 2.0f * Mathf.PI / 360;
            GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            sphere.transform.position = new Vector3(10 * Mathf.Cos(theta) * Mathf.Cos(phi),
                                                    10 * Mathf.Sin(phi),
                                                    10 * Mathf.Sin(theta) * Mathf.Cos(phi));
            float tokyu = float.Parse(c[10]);
            tokyu = 0.6f - tokyu / 10;
            if (tokyu < 0) tokyu = 0;
            sphere.transform.localScale = new Vector3(tokyu, tokyu, tokyu);
            sphere.GetComponent<Renderer>().material = _material[Color(c[2])];
        }
    }
}
```

　これを適当なゲームオブジェクトにアタッチすれば、ゲーム開始時に約3,000個の星（球体）がシーンに描画されます。実際やろうとすると、後述の通りいくらか下処理が必要ですが。

# 事前データ処理

　先に挙げたデータベースから`hip_lite_major.csv`（約3,000行）のデータを使いますが、これには恒星の名前が入っていません。すべての恒星には[ヒッパルコス星表](https://ja.wikipedia.org/wiki/%E3%83%92%E3%83%83%E3%83%91%E3%83%AB%E3%82%B3%E3%82%B9%E6%98%9F%E8%A1%A8)という表によって番号（HIP番号）がつけられているらしいですが（すごい！）、当然ながらその中で名前をつけられるような目立つ星はごく一部です。そのデータは`hip_proper_name.csv`にまた別にまとめられているのですが、両者をいわゆるリレーショナル結合する必要があります。幸いHIP番号がプライマリキーとなるので処理はすぐ済みます。

　ここら辺の下処理はpythonと`pandas`でサクッとやります。

```python
import pandas as pd
data = pd.read_csv('hip_proper_name.csv',header=None)
detail = pd.read_csv('hip_lite_major.csv',header=None)
mdata = pd.merge(detail,data,on=0,how="left")
mdata.to_csv('mdata.csv',header=None)
```

　これで両者がマージされた`mdata.csv`が出来上がりますので、これをUnityプロジェクトに入れておきます。

　あとマテリアルの設定もUnity側で手作業でやっておきます。がんばればスクリプトで記述できるとは思いますが、まあさすがにここら辺は妥協してもよいでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/2dee0d0e-5c97-ace8-595e-39d4dfce53f5.png)

　星座の色のマテリアルを作って、スクリプトをアタッチするようの`GameObject`のMaterialスロットに設定しておきます。

# ソースコード解説

```C#
    //特定の星座に色をつけるためのリスト
    List<string> Spring = new List<string> { "Arcturus" , "Spica" , "Denebola" };
    List<string> Summer = new List<string> { "Deneb", "Altair", "Vega" };
    List<string> Autumn = new List<string> { "Markab", "Sheat", "Algenib" , "Alpheratz" };
    List<string> Winter = new List<string> { "Capella", "Aldebaran", "Rigel", "Pollux" ,"Procyon" , "Sirius" };
```

　星座（星座ではなくて複数の恒星をまたいだなにかなのですが、他にうまい名称がみつからないので星座とさせていただきます）の名前はさっきのcsvに乗っていないので、自前で用意する必要があります。それぞれの星座をリスト化しておきます。

　csvファイルを読み込む方法については、https://note.com/macgyverthink/n/n83943f3bad60 を全面的に参考にさせていただきました。ありがとうございます。

　csvの各行から、角度とスケールを計算していきます。

　角度については、データベースに赤経（時・分・秒）と赤緯（度・分・秒）があるのでそこから計算します。度分秒なので少しややこしいですが、赤緯の角度$\phi$は、

$$
\phi = 度 + \frac{分}{60} + \frac{秒}{3600}
$$

　であることは調べればわかります。
　赤経$\theta$については、**時**・分・秒になっていて、これは調べても出てこなかったがですが、要は360度を24分割しているので、**1時あたり15度**ということになります。よって上の式に$15$をかければいいことがわかります。

$$
\theta = 時*15 + \frac{分}{4} + \frac{秒}{240} 
$$

　以上で計算できます。
　Unityの`Rotate`はラジアン単位であることと、更に座標系が左手系であることに注意すれば、

```c#
            theta *= 2.0f * Mathf.PI / 360;
            （中略）
            phi *= 2.0f * Mathf.PI / 360;
            （中略）
            sphere.transform.position = new Vector3(10 * Mathf.Cos(theta) * Mathf.Cos(phi),
                                                    10 * Mathf.Sin(phi),
                                                    10 * Mathf.Sin(theta) * Mathf.Cos(phi));
```

　で各恒星を正しい座標に配置することができます。
　また、各等級による明るさの違いを球の大きさで示しています。

```c#
            float tokyu = float.Parse(c[10]);
            tokyu = 0.6f - tokyu / 10;
            if (tokyu < 0) tokyu = 0;
            sphere.transform.localScale = new Vector3(tokyu, tokyu, tokyu);
```

　これでだいぶ星空感が出てきます。

# やりたいこと

　せっかくUnity上のデータとして扱えるので、表示する星を操作したり、星座の線が浮かび上がるような仕組みができたらインタラクティブなプラネタリウムが実現できますね。それをQuestでビルドしたりすれば、野外でもスタンドアロンで簡単プラネタリウム（そしてヘッドセットを外す度に現実の星空のショボさに落胆する）みたいなことができそうです。時間と気力があったらやってみたいです。

# 追記（リリースしました）

　その後、天文に詳しい友人に見てもらい、サイズ感や色味等を調整した後WebGLアプリとしてリリースしました。
　最新版のコード。

```c#
using System.Collections.Generic;
using UnityEngine;
using System.IO;

public class Reader : MonoBehaviour
{
    TextAsset csvFile;
    List<string[]> csvDatas = new List<string[]>();
    public Material[] _material;

    List<string> Spring = new List<string> { "Arcturus" , "Spica" , "Denebola" };
    List<string> Summer = new List<string> { "Deneb", "Altair", "Vega" };
    List<string> Autumn = new List<string> { "Markab", "Sheat", "Algenib" , "Alpheratz" };
    List<string> Winter = new List<string> { "Capella", "Aldebaran", "Rigel", "Pollux" ,"Procyon" , "Sirius" };

    int Colorization(string target)
    {
        foreach (string c in Spring) if (target == c) return 1;
        foreach (string c in Summer) if (target == c) return 2;
        foreach (string c in Autumn) if (target == c) return 3;
        foreach (string c in Winter) if (target == c) return 4;
        return 0;
    }

    Color Spectrum(string s)
    {
        if(s == "")
        {
            return new Color(.5f, .5f, .5f, 1);
        }
        char c = s[0];
        if(c == 'O')
        {
            return new Color(.3f, .3f, .8f,1);
        }
        else if (c == 'B')
        {
            return new Color(.3f, .3f, .8f, 1);
        }
        else if (c == 'A')
        {
            return new Color(.5f, .5f, 1, 1);
        }
        else if (c == 'F')
        {
            return new Color(1, 1, 1, 1);
        }
        else if (c == 'G')
        {
            return new Color(.8f, .8f, .4f, 1);
        }
        else if (c == 'K')
        {
            return new Color(.8f, .4f, 0, 1);
        }
        else if(c == 'M')
        {
            return new Color(.4f, 0, 0, 0);
        }
        else
        {
            return new Color(.5f, .5f, .5f, 1);
        }
    }
    
    void Start()
    {
        csvFile = Resources.Load("mdata") as TextAsset;
        StringReader reader = new StringReader(csvFile.text);
        while (reader.Peek() != -1)
        {
            string line = reader.ReadLine();
            csvDatas.Add(line.Split(','));
        }

        foreach (string[] c in csvDatas)
        {
            int lon1 = int.Parse(c[3]);
            int lon2 = int.Parse(c[4]);
            float theta = lon1 * 15 + lon2 / 4;
            theta *= 2.0f * Mathf.PI / 360;
            int lat1 = int.Parse(c[7]);
            int lat2 = int.Parse(c[8]);
            float phi = lat1 + lat2 / 60;
            if (c[6] == "0") phi = -phi;
            phi *= 2.0f * Mathf.PI / 360;
            GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            sphere.transform.position = new Vector3(10 * Mathf.Cos(theta) * Mathf.Cos(phi),
                                                    10 * Mathf.Sin(phi),
                                                    10 * Mathf.Sin(theta) * Mathf.Cos(phi));
            float tokyu = float.Parse(c[10]);
            tokyu = 0.3f - tokyu / 20;
            if (tokyu < 0) tokyu = 0;
            sphere.transform.localScale = new Vector3(tokyu, tokyu, tokyu);
            MeshRenderer r = sphere.GetComponent<MeshRenderer>();
            r.material = _material[0];
            r.material.EnableKeyword("_EMISSION");
            Color StarColor = Spectrum(c[11]) * tokyu / 0.3f;
            if (float.Parse(c[10]) <= 1.5f)
            {
                r.material.SetColor("_EmissionColor", StarColor * .3f);
            }
            
            //sphere.GetComponent<Renderer>().material = _material[Colorization(c[2])];
        }
    }
}
```

　恒星の色は[スペクトル分類](https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%9A%E3%82%AF%E3%83%88%E3%83%AB%E5%88%86%E9%A1%9E)と対応づけています。OやKといった文字で分類されているので、ひたすらif文で処理。それを`r.material.SetColor("_EmissionColor", StarColor * .3f);`でマテリアルのエミッションカラーに設定することにより色を調節しています。

　GitHubのリポジトリは[ここ](https://github.com/hibit-at/StarSphere)になります。

# やりたいこと2

- VRアプリ化
- 星座線の表示