WxGL アプリケーション例: 高精度 3D 太陽系モデルの描画

どうしたの?十二点?太陽と月は安全ですか?嘘のシンガン・チェン?—— ク元

太古の人類は、神秘的で深い星空に好奇心と畏怖の念に満ちていました。星空を見上げれば、宇宙は広大です。宇宙よりも素晴らしいのは、人類のたゆまぬ宇宙への追求と、遺伝子に宿る探究心です。

この記事では、WxGL を使用して、「Sun と Moon は安全で、Lie Xing は安全で、Chen は安全である」という古い質問に答えようとします。太陽系の天体軌道データはJPL(アメリカのジェット研究所)のエフェメリスから、天体の自転周期や自転軸の傾きはインターネットから取得しています。このモデルの天体の軌道と自転軸の傾きの精度は、以下の 2 つの Web サイトで相互に検証できます。

1 座標系の選択

星空を見上げるとき、見落としがちな重要な前提があります。古代人は自然に地球の上に立って星空を見上げることを選択したため、ジオセントリック理論と天球モデルが生まれました。このモデルは、天の川のある地点に立ち、太陽との相対的な位置関係を維持し、神の視点から太陽系全体を観察することを選択します。

どこに立っているとしても、通常は天体座標系と呼ばれる座標系が必要です。これは、太陽系内のさまざまな天体の相対的な関係を調整および記述するために使用されます。多くの一般的に使用される天体座標系があります.このモデルは黄道座標系を使用して天体の軌道を記述し、最終的にそれを WxGL が出力を描画するために使用する 3 次元空間直交座標系に変換します.

1.1 黄道座標系

いわゆる黄道は天球上の太陽の軌道であり、黄道が位置する平面は黄道面と呼ばれます。神の視点からは、黄道面は地球の公転軌道面です。傾いた地球が示す姿勢のように、地球の赤道面と黄道面は一致せず、両者の間に 23°26' の夾角があり、これが黄道角です。

黄道座標系(Ecliptic Coordinate System)は、黄道面を基準面とし、春分点を原点とする天体座標系で、時間や観測者の位置によって座標が変化しません。

ここに画像の説明を挿入

黄道座標系

X が天球上の天体であり、X' が黄道面上の X の投影である場合、黄道座標系における X の座標は 2 つのパラメーターによって定義できます。

  • 黄経 (λ と表示): 角度 VX'。春分点から反時計回りの方向が正で、値の範囲は 0° から 360° です。
  • 黄緯度 (黄道緯度、β と表記): 角度 XX'、つまり、天体と太陽を結ぶ線と黄道面の間の角度 (線平面角度)、値の範囲は +90° ~ - 90°。

1.2 三次元空間におけるデカルト座標系

OpenGL と同様に、WxGL は右手座標系を使用し、デフォルトの y 軸は高さ軸です。haxis パラメーターを使用して z 軸を高さ軸として設定し、azim パラメーターと elev パラメーターを使用して初期の方位角と仰角を変更し、fovy パラメーターを使用して初期の水平視野幅を設定します。このモデルは、高さ軸として z 軸を取ります。

import wxgl
app = wxgl.App(haxis='z', azim=15, elev=10, fovy=50)
app.axes()
app.show()

下図の左右は、y軸とz軸を高さ軸とする座標系で、初期方位角は15°、初期仰角は10°です。

ここに画像の説明を挿入

左: Y 軸は高さ軸、右: Z 軸は高さ軸

2 JPLエフェメリスを使った軌道計算

JPL エフェメリスは、1960 年代に米国のジェット推進研究所によって確立され、太陽、月、8 つの惑星、冥王星の過去と未来の位置に関する情報を提供します。JPLエフェメリスの本来の目的は惑星探査と航法であり、観測技術の継続的な改善と新しい観測データの継続的な取得により、JPLエフェメリスは現在も改訂および改善されています。

この記事では DE405 エフェメリス テーブルを使用します。ダウンロード アドレスはhttps://github.com/skyfielders/python-skyfield/blob/master/ci/de405.bspで、ファイルは約 65M で、西暦 1600 年から太陽系をカバーしています。一般的に適用される要件を満たすための 2200 AD データ。

Python にはエフェメリス データを読み取ることができる多くのモジュールがあり、skyfield を使用するのが好きです。インストールも非常に簡単です。

pip install skyfield

2.1 日時

JPL エフェメリスはユリウス日 (ユリウス エポック) を使用するため、ユリウス日と UTC 時間を変換するツールが必要であり、skyfield モジュールがこのツールを提供します。このツールの使用法は、以下の Python IDLE で示されています。

>>> from datetime import datetime, timedelta
>>> from skyfield.api import load, utc
>>> ts = load.timescale() # 创建一个时间处理工具
>>> ts.now() # 当前时刻的儒略历对象
<Time tt=2460055.7605328355>
>>> ts.utc(2023, 4, 21, 8, 0, 0) # 指定日期时间生成儒略历对象
<Time tt=2460055.834134074>
>>> dt = datetime.fromisoformat('2023-04-21 08:00:00') # 生成一个datetime对象
>>> t = ts.from_datetime(dt.replace(tzinfo=utc)) # 从datetime对象转化为儒略日对象
>>> t.utc_iso() # 转为UTC字符串
'2023-04-21T08:00:00Z'
>>> ts.utc(2023, 4, 21, range(8)) # 生成一个长度为8时间序列,间隔1小时
<Time tt=[2460055.500800741 ... 2460055.7924674074] len=8>

2.2 特定の瞬間における天体の位置

地球を例にとると、2023 年 5 月 1 日 (UTC 時間) 0:00:00 の位置を計算するには、次のコードを使用します。

from skyfield.api import load

ts = load.timescale()
t = ts.utc(2023, 5, 1, 0, 0 ,0)
planets = load('res/de405.bsp') # 读星历表
earth = planets['earth'] # 地球对象
print(earth.at(t).ecliptic_xyz().au) # t时刻地球的位置,使用天文单位(日地距离)
print(earth.at(t).ecliptic_xyz().km) # t时刻地球的位置,使用天文单位(日地距离)

走行結果は以下の通り。

xufive@xuxiangwudeMacBook-Pro solar % python3 orbit_demo.py
[-7.79775078e-01 -6.49301881e-01  2.49865838e-04]
[-1.16652691e+08 -9.71341788e+07  3.73793973e+04]

2.3 天体の軌道

次のコードは、2023 年 5 月 1 日 0:00:00 (UTC 時間) から 1 秒間隔で 1 時間以内の地球の軌道を示します。

from skyfield.api import load

ts = load.timescale()
t = ts.utc(2023, 5, 1, 0, 0, range(3600))
planets = load('res/de405.bsp') # 读星历表
earth = planets['earth'] # 地球对象
orbit = earth.at(t).ecliptic_xyz().km
print(orbit.shape)

計算結果の軌道は、ndarray 型の 2 次元配列です。

xufive@xuxiangwudeMacBook-Pro solar % python3 orbit_demo.py
(3, 3600)

3 太陽系モデル

3. 1 太陽と 8 つの惑星の家族写真

想定される太陽系モデルには、太陽、月、および 8 つの惑星が含まれます。最大の天体は当然太陽で、半径は約 696,000 キロメートルです。最小の天体は月で、半径はわずか 1,738 キロメートルで、約 400 分の 1 です。太陽の。しかし、その差は天体間の距離の差に比べれば大したことではありません。海王星は太陽から約 45 億キロメートル離れており、地球と月の間の距離 (38 万キロメートル) の約 12,000 倍です。

このようにダイナミック レンジが大きいと、等比率で描かれた太陽系モデルは基本的に無意味になるため、ほとんどのモデルは天体の半径と距離をスケーリングします。このモデルも例外ではありませんが、縮尺率は自由に選択でき、1を選択すると等縮尺モデルになります。

直感的なコンセプトを確立するために、まず太陽の前に立っている 8 つの惑星の集合写真を撮り、それらの身長の視覚的な違いがどれほど大きいかを確認します。前列左から水星、金星、地球、火星、木星、土星、天王星、海王星で、一番小さい月は集合写真の対象外です。

ここに画像の説明を挿入

太陽と8つの惑星の家族の肖像画)

この画像を生成するコードは非常に単純で、わずか 13 行です。

import wxgl
app = wxgl.App(fovy=18) # 此处默认y轴为高度轴
app.sphere((0,0,0), 696300, texture='res/sun.jpg', transform=lambda t:((0,1,0,(0.002*t)%360),))
app.sphere((-535304,0,594516), 2440, texture='res/mercury.jpg')
app.sphere((-400000,0,692820), 6052, texture='res/venus.jpg')
app.sphere((-247214,0,760845), 6371, texture='res/earth.jpg')
app.sphere((-83623,0,795618), 3398, texture='res/mars.jpg')
app.sphere((83623,0,795618), 71492, texture='res/jupiter.jpg')
app.sphere((247214,0,760845), 60268, texture='res/saturn.jpg')
app.sphere((400000,0,692820), 25559, texture='res/uranus.jpg')
app.sphere((535304,0,594516), 24718, texture='res/neptune.jpg')
app.show()

シンプルですが、関数をパラメーターとして使用するという WxGL の特徴を示しています。太陽描画関数 sphere の変換パラメータが関数であるため, 描画された太陽は回転しています. ここで使用されるラムダ関数は通常の関数も使用できます. この関数は時間 t をパラメータとして取り, で高さ軸の周りを回転する太陽を返します.時間 t 角度。

このコードを実行するには、各天体のテクスチャ画像を使用する必要があります. 以下に示す素材は、ローカルで直接ダウンロードするか、https://github.com/xufive/wxglの例からダウンロードできます.
ここに画像の説明を挿入

太陽

ここに画像の説明を挿入

水星

ここに画像の説明を挿入

金星

ここに画像の説明を挿入

地球

ここに画像の説明を挿入

火星

ここに画像の説明を挿入

木星

ここに画像の説明を挿入

土星

ここに画像の説明を挿入

天王星

ここに画像の説明を挿入

ネプチューン

ここに画像の説明を挿入


3.2 時間、距離、半径のスケーリング

地球が太陽の周りを公転するのに 1 年かかり、モデルは地球の公転をすばやく実現する必要があるため、時間加速係数を設定する必要があります。モデルの 1 秒が実際の時間の 1 日を表すために使用される場合、加速係数は 86400 であるため、モデルの地球は太陽の周りを約 6 分で回転できます。しかし、これにより、モデル内の地球が 1 秒に 1 回自転することになり、あまりにも速く、はっきりと見ることができなくなります。したがって、2800 の時間係数の選択 (モデルの 1 秒は実際の時間の 8 時間に相当) は妥協です。

海王星などの惑星は太陽から離れすぎており、実際の縮尺通りにモデルを描くと、太陽さえも小さく見えてしまう可能性があるため、惑星と惑星の距離を縮小する必要があります。太陽。この倍率は、内惑星 (火星の軌道内の惑星、つまり火星、地球、金星、水星) が互いに圧迫しないように 1/50 に選択されています。

水星や月などのより小さな天体を観測できるようにするためには、太陽以外の天体の半径を大きくする必要がありますが、木星や土星などの惑星がその大きさを超えることは適していません。そのため、半径の倍率として 20 を選択する方が適切です。

天体の半径を大きくし、天体間の距離を縮めると、地球と月が部分的に重なったり、月が完全に地球に飲み込まれたりする可能性があります。距離と半径のスケーリング係数を決定した後、地球と月が重なっているかどうかを評価する必要があります。重なっている場合は、地球と月の間の距離を大きくする必要があります。

3.3 黄道座標系モデル

太陽系モデルコードは約180行。完全なコード、画像、エフェメリス、およびその他のリソース ファイルは、https://github.com/xufive/wxglからダウンロードできます。特記事項: このコードは最新バージョンの WxGL の新機能を使用しています。コードを実行するには、必ず WxGL をバージョン 0.9.12 に更新してください。

import wxgl
import numpy as np
from skyfield.api import load, utc
from datetime import datetime, timedelta

class SolarSystemModel:
    """太阳系天体轨道计算类"""

     # 天体常量:半径r(km)、公转周期revo(太阳日)、自转周期spin(小时)和自转轴倾角tilt(度,相对于黄道面)
    CONST = {
    
    
        'sun':      {
    
    'r': 696300,   'revo': 0,          'spin': 24*24.47,   'tilt': 7},
        'mercury':  {
    
    'r': 2440,     'revo': 87.99,      'spin': 24*58.6,    'tilt': 0},
        'venus':    {
    
    'r': 6052,     'revo': 224.70,     'spin': 24*243,    'tilt': 177.3},
        'earth':    {
    
    'r': 6371,     'revo': 365.2564,   'spin': 23.934,     'tilt': 23.43},
        'mars':     {
    
    'r': 3398,     'revo': 686.97,     'spin': 24.617,     'tilt': 25.2},
        'jupiter':  {
    
    'r': 71492,    'revo': 4332.71,    'spin': 9.833,      'tilt': 3.1},
        'saturn':   {
    
    'r': 60268,    'revo': 10759.5,    'spin': 10.233,     'tilt': 26.7},
        'uranus':   {
    
    'r': 25559,    'revo': 30685,      'spin': 17.24,     'tilt': 97.8},
        'neptune':  {
    
    'r': 24718,    'revo': 60194.25,   'spin': 15.966,     'tilt': 28.3},
        'moon':     {
    
    'r': 1738,     'revo': 27.32,      'spin': 24*27.32,   'tilt': 1.5424}
    }

    def __init__(self, de_file, t_factor=28800, d_factor=1/50, r_factor=20, start_dt=None):
        """构造函数

        de_file         - JPL星历表文件
        t_factor        - 时间加速因子,默认模型中的1秒对应实际时间的28800秒(8小时)
        d_factor        - 距离缩放因子,默认以实际天体间距离的1/50绘制模型
        r_factor        - 除太阳外其他天体半径缩放因子,默认以实际半径的20倍绘制模型
        start_dt        - 开始日期时间字符串(YYYY-MM-DD hh:mm:ss),默认None,表示当前UTC时刻开始
        """

        self.t_factor = t_factor
        self.d_factor = d_factor
        self.r_factor = r_factor
        self.start_dt = datetime.utcnow().replace(tzinfo=utc) if start_dt is None else datetime.fromisoformat(start_dt).replace(tzinfo=utc)
        
        self.ts = load.timescale() # 创建处理时间的对象
        self.planets = load(de_file) # 加载星历文件
        self.k = 20000 * r_factor / (380000 * d_factor) # 地月距离缩放系数

    def get_ecliptic_xyz_at_dt(self, planet_name, dt):
        """根据日期时间计算天体在黄道坐标系中的坐标

        planet_name     - 天体名称
        dt              - datetime类型的日期时间
        """

        t = self.ts.from_datetime(dt)
        name = '%s_barycenter'%planet_name if planet_name in ('jupiter', 'saturn', 'uranus', 'neptune') else planet_name
        x, y, z = self.planets[name].at(t).ecliptic_xyz().km

        return x*self.d_factor, y*self.d_factor, z*self.d_factor

    def get_ecliptic_xyz(self, planet_name, time_delta):
        """计算天体在黄道坐标系中的坐标

        planet_name     - 天体名称
        time_delta      - 距离开始时刻的时间偏移量,以为毫秒单位
        """

        seconds = self.t_factor * time_delta / 1000 # 模型时间转换为实际时间偏移量
        dt = self.start_dt + timedelta(seconds=seconds)

        return self.get_ecliptic_xyz_at_dt(planet_name, dt)

    def get_ecliptic_orbit(self, planet_name):
        """计算天体单个公转周期的运行轨道,planet_name为天体名称"""

        days = round(self.CONST[planet_name]['revo'] - 366)
        dt = self.start_dt - timedelta(days=days) if days > 0 else self.start_dt 
        hours = np.linspace(0, self.CONST[planet_name]['revo'], 1001) * 24
        t = self.ts.utc(dt.year, dt.month, dt.day, hours)
        name = '%s_barycenter'%planet_name if planet_name in ('jupiter', 'saturn', 'uranus', 'neptune') else planet_name
        x, y, z = self.planets[name].at(t).ecliptic_xyz().km

        return np.stack((x*self.d_factor, y*self.d_factor, z*self.d_factor), axis=1)

    def get_sphere(self, planet_name):
        """返回天体球面网格的顶点坐标,planet_name为天体名称"""

        r = self.CONST[planet_name]['r'] if planet_name == 'sun' else  self.CONST[planet_name]['r'] * self.r_factor
        gv, gu = np.mgrid[np.pi/2:-np.pi/2:37j, 0:2*np.pi:73j]

        zs = r * np.sin(gv)
        xs = r * np.cos(gv) * np.cos(gu)
        ys = r * np.cos(gv) * np.sin(gu)

        return xs, ys, zs

    def get_axis(self, planet_name):
        """返回天体旋转轴的顶点坐标,planet_name为天体名称"""

        r = self.CONST[planet_name]['r'] * self.r_factor
        
        return [[0, 0, 1.5*r], [0, 0, -1.5*r]]

    def dt_func(self, t):
        """格式化日期时间的函数,用于在UI的状态栏显示模型对应的UTC时间"""

        return self.ts.from_datetime(self.start_dt + timedelta(seconds=self.t_factor*t/1000)).utc_iso()

    def tf_sun(self, t):
        """太阳模型变换函数,实现自转"""
    
        speed = 0.36 / (self.CONST['sun']['spin'] * 3600 / self.t_factor)
        phi = (t * speed) % 360

        return ((0, 0, 1, phi), )
    
    def tf_moon(self, t):
        """月球模型变换函数,跟随地球运动的同时实现自转、旋转轴倾斜和公转"""
    
        rotate = (0, 0, 1, (t * 0.36 / (self.CONST['moon']['spin'] * 3600 / self.t_factor)) % 360)
        tile = (-1, 0, 0, self.CONST['moon']['tilt'])
        xm, ym, zm = self.get_ecliptic_xyz('moon', t)
        xe, ye, ze = self.get_ecliptic_xyz('earth', t)
        shift = xe+(xm-xe)*self.k, ye+(ym-ye)*self.k, ze+(zm-ze)*self.k

        return (rotate, tile, shift)

    def tf_factory(self, planet_name):
        """天体模型变换函数生成器,返回实现天体自转、旋转轴倾斜、公转的变换函数"""
    
        def tf(t):
            rotate = (0, 0, 1, (t * 0.36 / (self.CONST[planet_name]['spin'] * 3600 / self.t_factor)) % 360)
            tile = (-1, 0, 0, self.CONST[planet_name]['tilt'])
            shift = self.get_ecliptic_xyz(planet_name, t)
    
            return (rotate, tile, shift)

        return tf
    
    def show_ecs(self):
        """绘制黄道坐标系模型"""

        app = wxgl.App(haxis='z', elev=15, fovy=35, backend='qt')
        app.title('太阳系模型')
        app.info(time_func=self.dt_func) # 在状态栏中显示日期时间信息
        
        # 绘制太阳
        xs, ys, zs = self.get_sphere('sun')
        app.mesh(xs, ys, zs, texture='res/sun.jpg', light=wxgl.BaseLight(), transform=self.tf_sun)

        # 绘制月球
        xs, ys, zs = self.get_sphere('moon')
        app.mesh(xs, ys, zs, texture='res/moon.jpg', light=wxgl.BaseLight(), transform=self.tf_moon)
        
        # 绘制8个行星
        names = ('mercury', 'venus', 'earth', 'mars', 'jupiter', 'saturn', 'uranus', 'neptune')
        colors = ('dodgerblue', 'gold', 'cyan', 'firebrick', 'burlywood', 'darksalmon', 'lightgray', 'lightskyblue')
        for name, color in zip(names, colors):
            # 绘制行星
            xs, ys, zs = self.get_sphere(name)
            app.mesh(xs, ys, zs, texture='res/%s.jpg'%name, light=wxgl.BaseLight(), transform=self.tf_factory(name))
            
            # 绘制行星自转轴
            vs = self.get_axis(name)
            app.line(vs, color=color, stipple='dash-dot', transform=self.tf_factory(name))
        
            # 绘制行星公转轨道线
            orbit = self.get_ecliptic_orbit(name)
            app.line(orbit, color=color)

        # 绘制春分、秋分、夏至和冬至标识
        for dt_str, word in (('03-21','春分'), ('06-22','夏至'), ('09-22','秋分'), ('12-23','冬至')):
            dt = datetime.fromisoformat('%d-%s'%(self.start_dt.year, dt_str)).replace(tzinfo=utc)
            x, y, z = self.get_ecliptic_xyz_at_dt('earth', dt)
            d = 4000 * self.r_factor
            box = [[x-2*d, y, z-d], [x-2*d, y, z-2*d], [x+2*d, y, z-2*d], [x+2*d, y, z-d]]

            app.line([[x, y, z+d], [x, y, z-d]], color='white', width=2)
            app.text3d(word, box, align='center')

        app.show()

if __name__ == '__main__':
    ssm = SolarSystemModel('res/de405.bsp', d_factor=1/50, r_factor=20, start_dt='1962-02-05 01:00:00') # 该日期七星连珠,排列在9.3°范围内
    ssm.show_ecs()

モデルを描画するとき、省略できないエフェメリス ファイル パラメータを除いて、他のパラメータはオプションです。コードで指定された日付は、歴史上有名な七星連珠の日です。日付を省略すると、現時点からモデルが描画されます。

下の画像は太陽系のパノラマです。火星の軌道内の惑星と太陽は真ん中にあり、一緒に見ることはほとんどできず、木星、土星、天王星、海王星の外側の軌道だけがはっきりと見えます。
ここに画像の説明を挿入

太陽系モデル 図 1

マウス ホイールを回転させて視野を縮小します。これは、観測者に望遠鏡を装着させ、最終的に内惑星を見ることに相当します。
ここに画像の説明を挿入

太陽系モデル 図 2

ホイールをスクロールし続けます - 高倍率の望遠鏡を使用すると、月も明らかになります。
ここに画像の説明を挿入

太陽系モデル 図 3

ズームインを続けて、最後に太陽-地球-月系の動きを確認してください。これは、「太陽と月は安全で、星は安全でチェン」という質問に対する答えと見なすことができます。
ここに画像の説明を挿入

太陽系モデル 図 4

おすすめ

転載: blog.csdn.net/xufive/article/details/130126117