ブラウザに本物の夜空を描く第2節/全10節
天文計算 — 星はどこにあるか
地点と時刻から天体の方向を出す計算を、恒星はビルド時に、太陽系天体は毎フレームにと二つの経路へ分けています。
「実際の夜空を描く」というとき、計算しているのは一つのことです。ある地点・ある時刻に、その天体がどちらの方向に見えるか。この節では、その方向をどんな量から導いているか、恒星と太陽系天体でなぜ計算の経路を分けたか、どこまでを既存のライブラリに委ねたかを説明します。
その計算はすべてAsterariumで、three.jsもReactも状態管理の仕組みも参照しない、天文計算などのモジュール群を指す呼び名です。ブラウザを立ち上げずに実行できるので、入力を与えて戻り値を確かめるだけのテストが書けます。に閉じています。分けている理由はテストです。外部の状態に触れず、ブラウザもブラウザの中からGPU(絵を描くための専用チップ)を直接使って3Dの絵を描く、Web標準の描画APIです。プラグインなしで、ページの中に立体的な場面を出せます。も要らない関数の集まりにしておけば、Node上のテストランナーでそのまま走り、天文年鑑(その年の天体の位置を表にした刊行物。暦書とも呼びます)に載る値や、別実装の計算結果と直接突き合わせられます。
天球と、二つの座標系
天体は、観測者を中心とする半径一定の仮想的な球面、すなわち天文学で、星の見える向きを表すために考える、観測者を中心とした仮想の球面です。距離は持たず、星までの本当の距離は捨てて、どの向きに見えるかだけを扱います。この記事では、その天球を表す3Dシーンの入れ物も同じ名前で呼び、シーンの1単位が約1メートルにあたるので、半径1000、およそ1kmの球面に星を置いています。の上の一点として扱います。Asterariumのシーンでも天球は実在していて、恒星は半径1000の球面上に置かれます。シーンの長さは1単位が1メートル相当と決めてあるので(この取り決めは「座標系とシーンの姿勢」の節にあります)、これは半径約1kmの球面ということになります。この数字は見え方には影響しません。半径は星の向きさえ保てばよいからです。条件は一つだけ、カメラが描く範囲の内側に収まることです。その範囲は、手前がカメラの手前側の切り取り面で、これより近いものは描かれません。近くに置くほど遠方の距離の精度が落ちるので、どこに置くかが遠くの前後関係の正しさを左右します。の2単位、つまり2メートル。奥がカメラの奥側の切り取り面で、これより遠いものは描かれません。手前側の切り取り面との比が、遠くにあるものの前後を見分ける精度を決めるので、置く位置は必要な最遠距離ぎりぎりに選びます。で、1e7単位、つまり1千万単位、およそ1万kmです。
天球上の一点を表す角度の組は、基準の取り方で何通りもあります。使うのは二つです。地球の赤道と自転軸を空へ延ばして張る座標系です。位置は、経度にあたる赤経と緯度にあたる赤緯の2つの角度で表します。地球の自転とともには回らないので、恒星の値はほぼ一定です。は地球の自転軸を基準に取り、天の赤道(地球の赤道を空へ延ばした線)に沿って、春分点(天の赤道と太陽の通り道が交わる点)から東回りに測る角度で、経度にあたります。慣習として時分秒で書き、24時が360度にあたります。と天の赤道(地球の赤道を空へ延ばした線)を0度として、北を正、南を負に±90度まで測る角度で、緯度にあたります。で位置を表します。観測者の地平線を基準にした座標系で、位置を、真北から測る方位と、地平線から測る高度という2つの角度で表します。同じ星でも、場所や時刻が変われば値が変わります。は観測者の足元を基準に取り、観測者の地平線に沿って、真北を0度として東回りに測った角度です。東が90度、南が180度、西が270度にあたります。(北が0°、東が90°)と観測者の地平線から測った、天体の上向きの角度です。0度が地平線、90度が真上の天頂で、負の値は地平線の下にあることを意味します。(地平線が0°、天頂が+90°)で表します。
二つを分けるのは、時間に対するふるまいです。恒星の赤経・赤緯はほとんど変わりません。地球の自転が変えるのは観測者の向きであって、星そのものの位置ではないからです。逆に地平座標は数分で目に見えて変わります。星のカタログが赤道座標で配布されるのはこのためで、Asterariumはその値をそのまま使います。
二つを結ぶのが春分点(天の赤道と太陽の通り道が交わる点)を基準にした地球の自転角を、時刻の形で表したものです。角度を時間の単位で言い換えたものなので、24時間が360度、1時間が15度にあたります。星が空のどこに見えるかは、太陽ではなくこの値で決まります(暦の上での話で、実装がこの値を直接使うかどうかは別です)。、空の上で、天の赤道(地球の赤道を空へ延ばした線)と太陽の1年の通り道が交わる2点のうち、太陽が南から北へ横切る方の点です。空で経度にあたる角度は、この点を0度の起点として測ります。を基準にした地球の自転角です。地球は太陽に対して24時間で一周しますが、遠い星に対しては約23時間56分で一周します。その差の約4分ぶん、同じ星が天体が観測地の子午線(真南と真北と天頂を通る線)を横切り、その日の最も高い位置に達することです。北半球では、このとき天体はたいてい真南に見えます。する時刻は毎日約4分ずつ早まり、1年でほぼ一周ぶんずれます。純粋な計算層はグリニッジの恒星時を天文ライブラリから取り、観測者の経度を足して地方恒星時にします。ただし、恒星を画面に出す経路はこの値を通りません。天球を回す数を縦横に並べた表で、3Dでは回転や移動といった座標の変換をひとまとめに表すのに使います。変換を続けて掛け合わせれば、いくつもの段階を1つの行列にまとめられます。は、太陽の光が大気中の分子や粒子にぶつかって、あらゆる向きへ散らばる現象です。青い光ほど強く散らばるので昼の空は青く、低い太陽の光は長い距離の大気を通るので朝夕は赤くなります。ライブラリ@takram/three-atmosphereが日付から作るからです。大気の見え方を計算するには地球の姿勢そのものが要るので、そのライブラリが地球の姿勢を表す行列をすでに持っている、という事情です。恒星時を明示的に使うのは、情報パネルの表示・天体の検索・照準のための簡易な変換と、投影機のページ(/projector/)の計算です。
見かけの位置を歪める三つの効果
幾何学的な位置と、実際に目に見える位置は少しずれます。主な原因は三つで、Asterariumは対象ごとに扱いを変えています。ずれの大きさは角度の単位で、1度の3600分の1です。満月の見かけの直径がおよそ1800秒角にあたります。と角度の単位で、1度の60分の1、すなわち60秒角です。で表します。見えるかどうかの目安には二つの水準があります。一つは、点が1つずれたと分かる限界で、およそ1分角、つまり60秒角。もう一つは、星座の形が変わったと分かる限界です。こちらは数十度に広がる星の並びの中での話で、星座線で結ばれる隣り合う星の間隔は目安として数度あります。その中では数分角のずれにはまだ気づけず、数十分角に達してようやく形の違いとして見えてきます。
- 観測者自身の運動によって、光の来る向きが観測者の進行方向へわずかに傾いて見える現象です。地球の公転によるずれは最大で約20秒角、つまり1度の180分の1ほどです。 — 観測者自身が動いているため、光の来る向きが進行方向へわずかに傾く現象。地球の公転による分は最大で約20秒角です。太陽系天体では計算に含めています。恒星ではカタログの天体の座標がいつ時点のものかを示す基準時刻、すなわち元期の一つで、世界時2000年1月1日正午を指します。星のカタログの座標は、多くがこの元期で書かれています。位置をそのまま使い、補正しません。
- 地球の大気が光を曲げるために、天体が実際の位置より高く見える現象です。持ち上がる量は地平線付近で最も大きく、約34分角(0.57度)に達します。 — 大気の屈折で天体が実際より高く見える現象。地平線付近で約34分角に達し、太陽や月の見かけの直径、およそ0.5度ほどもあります。太陽系天体では標準的な補正を入れて地平座標へ変換します。恒星の描画では入れていません。
- 観測する位置が変わると、天体の見える向きも変わることです。星までの距離を測るのに使うのは年周視差、つまり地球の公転にともなう1年周期のずれで、最も近い恒星でも1秒角(1度の3600分の1)に満たない小さな角です。描画で位置のずれとして効くのはもう一方の日周視差、すなわち観測者が地球の中心ではなく地表にいることで生じるずれで、月では最大約1度に達します。 — 観測地が地球の中心ではなく地表にあることによる方向のずれ。月では最大約1°に達するので無視できません。観測者の緯度・経度・標高を渡し、地球の中心からではなく、地表の観測地点を原点として見た位置のことです。月のように近い天体では、地球中心から見た向きとの差が最大で約1度に達します。な位置として計算します。恒星は遠すぎて完全に無視できます。
恒星に光行差と大気差を入れない理由は、それぞれ違います。光行差の約20秒角は、点1つのずれとして見える限界の3分の1しかなく、そもそも見えません。大気差はそうではありません。高度45°でも約1分角、地平線の近くではその30倍を超える約34分角に達します。それでも入れていないのは、大気差の量が高度によって変わるからです。高度ごとに量の違うずれは、天球をまるごと一つの回転で回す現在の経路では表せません。数万個の恒星は、まさにその一つの回転行列で回すだけで画面に出ます。行列の中身は、2000年の基準から当日の基準への地球の自転軸の向きが、約2万6000年かけて円を描くようにゆっくり変わる現象です。空の座標の起点である春分点もこれにともなって動くため、星の座標も少しずつ変わります。に地球の自転を掛けたものです(その組み立て方は「座標系とシーンの姿勢」の節にあります)。
とはいえ、道が閉じているわけではありません。星を描く形状の頂点1つ1つを画面上のどこへ置くかを計算する、GPU上の小さなプログラムです。頂点の数だけ実行されます。は、天球の回転行列を通した後の各星の向きをすでに計算していて、その上下成分は高度の正弦そのものです。実際そこでは、きらめきの強さを高度によって変えるのにこの値を使っています。同じ値から高度ごとの持ち上げ量を求めて星を上へずらすことは、原理的にはできます。実装していないだけです。
一方、太陽・月・惑星は1天体ずつ計算するので、大気差を入れています。この選び分けには、良い面と悪い面が両方あります。太陽そのものの高度は実際の空に近づきます — 沈む太陽が地平線の上に浮き上がって見えるのと同じ効果です。一方で背後の星との相対位置は、星に大気差を入れていないぶん実際の空とずれ、地平線の近くでは太陽や月が最大約34分角だけ星の並びより高く描かれます。後者を承知のうえで残しています。
二つの経路 — 恒星はビルド時に、太陽系天体は毎フレームに
恒星と太陽系天体では、位置の決まり方がまったく違います。恒星は数万個あるけれども、この用途では動かないとみなせる。このアプリが扱う太陽系天体は、太陽と月に、地球を除く7つの惑星、つまり水星・金星・火星・木星・土星・天王星・海王星を足した9個しかないけれども、速く動き、しかも恒星よりも高い精度 — 肉眼の限界にあたる分角級 — が要る。精度が要るのは、点で描く恒星と違って見かけの大きさを持つからです。月は直径約0.5度の円盤で、その欠け方も、惑星や恒星への近づき方も、数分角の差で目に見えて変わります。この非対称が、そのまま二つの経路になっています。
恒星の位置は配信前に確定します。決まるのはビルド時、利用者がページを開くよりも前です。ビルドスクリプトが恒星カタログAT-HYG(古くからあるHYG星表を、Tychoの測光やGaiaの視差といった観測で拡張した公開のカタログ)を読み、各星のJ2000赤道座標を長さを1にそろえた、向きだけを表すベクトルです。天体がどちらに見えるかのように、距離ではなく向きだけが要る量を扱うのに使います。へ変換し、二進のファイルへ焼き込みます。ランタイムでは、1星ずつ位置を計算しません。天球全体を一つの行列で回すだけです。だから星が何万個あっても、位置計算の仕事は行列を一つ組み立てることに変わりません。描画そのものの負荷は星の数に比例するので、そちらは端末の性能に応じて描く数を絞ります。絞るのはAsterariumで、描画の重さをまとめて切り替える設定です。低いほうから低・中・高・最高(コード上はlow・medium・high・ultra)の4段があり、描く星は低が明るい順に6,000星まで、中が明るい側の8,037星、高と最高が全38,168星で、画面の解像度の上限と星のまたたきの有無も段ごとに変わります。実測のフレームレートに応じて自動で上下します。の役目で、明るい順に並べておいて先頭から必要な数だけ描くというデータの作り方は「データパイプライン」の節にあります。
太陽系天体は逆に、毎動く絵を作るときの1コマです。滑らかに見せるには毎秒60コマ前後を描き続ける必要があり、1コマぶんの持ち時間は16ミリ秒ほどしかありません。計算します。太陽・月と、地球を除く水星から海王星までの7惑星、あわせて9天体について、bodyState()という関数が観測地と時刻から地平座標・その時点の地球の赤道と春分点(天の赤道と太陽の1年の通り道が交わる点)を基準にして表した座標です。地球の自転軸の向きがゆっくり変わるため、2000年時点の基準にそろえたJ2000の座標とは、年が離れるほど値がずれていきます。星図やカタログはJ2000で書かれ、いま空のどこに見えるかは当日基準で求めます。での赤経赤緯・天体の明るさを表す数値で、小さいほど明るく、1等級の差が約2.512倍の明るさにあたります。肉眼で見える限界は、暗い空でおよそ6等から6.5等です。・天体の見かけの大きさを角度で表したもので、円盤の半径にあたる角度です。太陽と月はどちらも視半径が約0.25度、つまり直径にすれば約0.5度でほぼ等しくなっています。月と地球の距離は一定ではないので月の視半径は前後し、月が大きいときの日食は太陽を覆い隠す皆既日食に、小さいときは縁が輪に残る金環日食になります。を返します。太陽以外の8天体には、天体そのものの位置から見た、太陽と観測者のなす角度です。月では0度が満月、180度が新月にあたります。円盤の光っている割合はこの角度で決まりますが、満ちる途中か欠ける途中かは区別できません。と天体の見かけの円盤のうち、光って見える部分の面積の割合です。0が新月、0.5が上弦や下弦の半月、1が満月にあたります。も付きます。9天体分をまとめて呼んで約0.12ミリ秒。開発時に手元の機械で計った、描画を含まない位置計算だけの値で、端末が変われば変わります。比べる相手は、描画まで含めたフレーム全体に許された時間 — 毎秒60回なら16.7ミリ秒 — です。位置計算はその1%に満たないので、フレームを間引くような小細工はしていません。
const time = MakeTime(new Date(t))
const observer = new Observer(loc.latDeg, loc.lonDeg, loc.elevationM)
const aeBody = BODY_MAP[body]
// Topocentric equatorial coordinates, of-date, with aberration.
const eq = Equator(aeBody, time, observer, true, true)
const hor = Horizon(time, observer, eq.ra, eq.dec, 'normal')
const illum = Illumination(aeBody, time)
// eq.dist is topocentric distance in AU. Angular radius = asin(R / d).
const distKm = eq.dist * KM_PER_AU
const angularRadiusDeg = (Math.asin(RADIUS_KM[body] / distKm) * 180) / Math.PI
const state: BodyState = {
horizontal: { azDeg: hor.azimuth, altDeg: hor.altitude },
equatorialOfDate: { raHours: eq.ra, decDeg: eq.dec },
mag: illum.mag,
angularRadiusDeg,
}
// Then: phase angle and illuminated fraction, for every body but the Sun.短いですが、この節で挙げた三つの補正がそのまま並んでいます。観測者を緯度・経度・標高から作るので位置は地表基準になり、これが視差の補正にあたります。赤道座標を求める呼び出しの末尾の二つのtrue引数は、2000年時点ではなく当日の赤道と春分点を基準にすること、つまり当日基準の座標で返すことと、光行差を含めることを指示します。地平座標へ移す呼び出しのnormalは標準的な大気差モデルの指定です。視半径だけは自前で、天体の半径と距離から求めています。
基準の取り方が恒星と食い違わないのか、という疑問が残ります。恒星は2000年時点の赤道と春分点を基準にした座標のまま、太陽系天体は当日基準の座標です。答えは、二つが別々の道を通って同じ当日の空に着く、です。恒星は天球全体を回す行列を通り、その行列が2000年の基準から当日の基準への歳差ぶんも一緒に動かします。太陽系天体はこの行列をまったく通りません。bodyState()が返す地平座標を、そのままAsterariumの3Dシーンで物の位置を表す座標系で、1単位がおよそ1メートル、原点は観測者、軸の取り方はNUE(North-Up-East、北・上・東)です。ここでの距離は実際の距離ではなく描画のための便宜的な値で、星は半径1000(約1km)の球面に、太陽・月・惑星はその外側の200万(約2,000km)に置きます。描かれる大気の層の厚さは約6万単位(60km)です。の方向ベクトルに直して置くだけです。地平座標はその時点の空を基準にした量なので、最初から当日の基準になっています。
その行列は大気散乱ライブラリが日付から作ります。大気の計算には地球の姿勢が要るので、ライブラリはこの行列をもともと持っていて、外へ公開しています。中身は前に触れたとおり、2000年の基準から当日の基準への歳差に、地球の自転を掛けたものです。歳差の量は2026年時点で約0.4度あり、星座の位置が肉眼で分かる程度に動く大きさなので、これが行列に入っているかどうかは効いてきます。
| 対象 | 位置を決める時点 | 位置の由来 | 入れている補正 |
|---|---|---|---|
| 恒星 | ビルド時に1回 | カタログのJ2000赤道座標 | 固有運動・光行差・大気差はいずれも未補正。視差は無視。歳差はシーンの回転行列が吸収 |
| 太陽・月・惑星 | 毎フレーム | 天文ライブラリによるその時点の計算 | 光行差・大気差・地表を基準にした視差 |
恒星に恒星が天球上を年々わずかに移動していくことです。全天で最も速いバーナード星でも1年に約10秒角、1度の360分の1ほどしか動きません。そのバーナード星も9.5等と暗く、8等より明るい星だけを載せたこの記事のカタログには含まれていません。を入れていないのも、その量が見えないからです。ほとんどの星の固有運動は年0.1秒角未満で、カタログの基準である2000年から2026年時点で26年たっても、移動は数秒角にとどまります。AT-HYGから抜き出しているのは8等より明るい約38,000星です。8等まで持つのは、実際に描く下限の6.8等に1.2等ぶんの余裕を足した数だからです(カタログをどう二段に分け、どの設定がどこまで描くかは「データパイプライン」の節にあります)。そのなかで最も速いのがグルームブリッジ1830の年約7.1秒角、明るい星ではアルクトゥルスの年約2.3秒角です。26年ぶんに直すと約3分角と約1分角。数十度の広がりを持つ星座の形が変わって見える大きさではありません。
同じ判断が、目的の違うページでは逆転します。恒星間フライトのページ(/starflight/)は固有運動そのものが主題なので、実測の固有運動と視線速度を持つ別のカタログを作り、前後10万年まで外挿します。無視してよいかどうかは星の性質ではなく、そのページが何を見せたいかで決まる、ということです。
どこまでをライブラリに任せたか
天体位置のエンジンはastronomy-engine一つだけです。決め手は三つありました。MITライセンスなので、サイトに含めて配布できること。時刻の多項式と三角関数の和として書き下した、天体の位置を近似する式です。式そのものが答えを出すので、あらかじめ計算した位置の表を読み込む必要がありません。だけで自己完結していて、あらかじめ計算した位置を並べた表を読み込まずに済むこと。そして公表されている精度が太陽系天体で1分角級、つまり肉眼の限界とほぼ同じであること。サーバー側で動くコードを持たないという前提に噛み合うのは、このうち前の二つです。ブラウザの中だけで計算が完結し、追加のダウンロードも増えません。精度のほうはサーバーの有無とは無関係で、肉眼で見える空を描くのに足りるかどうかという別の基準で測っています。ただし、要る精度も公表精度も1分角級だというのは、余裕が大きくないということでもあります。だから、分角の差がそのまま絵に出る日食・月食のページ(/eclipse/)は、この汎用の位置計算を使いません。同じライブラリが持つ日食専用の探索を呼んで、観測地ごとの接触時刻を直接得ます。毎フレームではなく、観測地を選んだときに一度だけ走る計算です。
ライブラリに委ねているのは、当日基準の座標での、地表を基準にした赤経赤緯、大気差込みの地平座標への変換、等級と位相角と輝面比、恒星時、出没時刻と太陽が地平線の下にありながら、空がまだ明るい時間帯です。太陽が沈むほど深くなり、夕方は高度−6度までの市民薄明、−12度までの航海薄明、−18度までの天文薄明の順に進みます。朝は逆に、天文薄明から航海薄明、市民薄明をたどって日の出に至ります。時刻の探索、月相、月の月が地球に対して、わずかに首を振るように向きを変えて見える動きです。このおかげで、時間をかければ月面の約59%を地球から見ることができます。です。自前で書いたのはその上に載る薄い層で、角度から単位ベクトルへの変換、月の欠けた月や惑星の、光っている側の縁のことです。輝縁は、天球上で太陽のある側を向きます。その向きを角度で言うのが輝縁の位置角で、明るい縁の中央が向く方向を、天の北から東回りに測ります。の位置角、1日分のイベントのまとめ方、そして低精度な地平座標変換です。
低精度版をわざわざ別に持っているのは、用途が違うからです。情報パネルの表示、天体の検索、そしてカメラをその天体へ向けるための照準は、恒星や星団のようにほぼ動かない対象について、赤道座標から地平座標への標準的な回転で方位・高度を出せば足ります。この回転を決めるのは観測地の緯度と地方恒星時だけです。大気差も光行差も入れません。太陽系天体だけはこの経路を使わず、必ずbodyState()を通す決まりです。恒星向けの簡易な地平座標変換と、太陽系天体向けのbodyState()を並べて持つ以上、どちらを何に使うかを決めておかないと取り違えが起きます。
等級、月、そして薄明
星の明るさは等級で表します。数字が小さいほど明るく、5等級の差がちょうど光量100倍と定義されています。1等級あたりの比はその5乗根、約2.512で、ポグソン比と呼ばれます。カタログが持つ等級を描画に使える線形の強度へ直すのは、次に挙げる関数のreturnの行、式にして一行です。やっていることは言葉にすれば一つで、等級が1つ増えるごとに強度を約2.512分の1にし、基準の等級でちょうど1にする、というだけです。その基準の等級は引数で選べます。星を描くGPU(絵を描くための専用チップ)の上で走る小さなプログラムで、頂点をどこへ置くか、画素をどんな色にするかを計算します。1枚の絵を作るあいだに、頂点や画素の数だけ繰り返し実行されます。は同じ式を6.5等基準で使い、暗い空で肉眼が見える限界あたりの星を強度1に置いています。
const POGSON = Math.pow(100, 1 / 5) // 2.511886431509…
export function magToIntensity(mag: number, refMag = 0): number {
return Math.pow(POGSON, refMag - mag)
}月については、位相角と輝面比のほかに、輝縁の位置角と秤動も計算しています。欠けた月の明るい側が画面のどちら向きになるかは、この輝縁の位置角で決まり、太陽と月の赤道座標から古典的な式で求めます。ただし満ち欠けの向きを決めるときに位相角は使いません。位相角は満月付近で0、新月付近で180になり、上弦でも下弦でも同じ90前後の値を取るので、行きと帰りを区別できないからです。代わりに月と太陽の太陽の1年の通り道である黄道に沿って、春分点(黄道と天の赤道が交わる点)から東回りに測った角度です。月や惑星の位置を扱うときの自然な経度になります。差 — 新月で0°、満月で180°、次の新月で360°と一方向に増える量 — を使います。
薄明は太陽の高度で三段階に分けます。市民薄明が−6°、航海薄明が−12°、天文薄明が−18°です。その日のイベントをまとめる関数は、日出没・月出没とあわせて、六つの薄明時刻 — 夕方に太陽の高度が各閾値を下向きに横切る時刻と、朝に上向きに横切る時刻 — を探索します。空の暗さそのものは大気散乱の計算が決めますが、いつ何が起きるかという時刻の側はここが持ちます。
高緯度では、太陽が一日中沈まない日も、一日中昇らない日もあります。そのときイベントの時刻は存在しません。純粋な計算層はそれを0や当日の終わりで埋めることをせず、フィールドを未定義のまま返します。呼び出し側に「その日は起きなかった」ことが伝わるので、白夜や極夜の空でも表示が破綻しません。存在しない値を無理に作らない、という規律です。
この節の判断は、どれも一つの問いに集約されます。どれだけの精度が、どの頻度で必要か。肉眼で見分けられない差は計算しない。見える差でも、描画の経路に載らないなら入れない。この線引きが、サーバー側で動くコードを持たない静的なページのまま、ブラウザだけで毎秒60回の空を描く余裕を作っています。