[Astro] #210 スマホと Cardboard で見上げるプラネタリウム(注視パネル・星の名前・場所と時刻の切り替え)

[Astro] #210 スマホと Cardboard で見上げるプラネタリウム(注視パネル・星の名前・場所と時刻の切り替え)

はじめに

スマホと Cardboard 型ゴーグル(PC でも動きます)で見る、PLANETARIUM の実装記録です。

  • 星空の真ん中に立って、見回します(スマホの傾きセンサー。PC はドラッグ)
  • 足元より少し下を見ると、時間を進める・戻すボタンが出ます。ボタンは見つめると押せます
  • 星を見つめると、名前・星座・等級・色・方角・高度が下に出ます
  • 場所(東京・シドニー・トロムソ)、±1 か月、表示の切り替え、太陽の通り道を、同じパネルで操作します

空の描画は、先に作った Sky Today の部品をそのまま読みます。Sky Today 側のファイルは変えていません。操作の枠は PLAZA と共通です(../common/cardboard.js、../lib/three-vrm.js)。

スクリーンショット

星を見つめると、名前が出る

ポルックスを見つめて、下に名前・星座・等級・色・方角・高度が出ている

足元の、時間と表示のパネル

見下ろすと出る、3 段の丸いボタン

太陽の通り道

トロムソの 9 月、太陽を通るオレンジの線

動画

1. 構成

public/tool/mobile/planetarium/
  index.html        cardboard.css + planetarium.css + planetarium.js
  planetarium.js    本体(先頭の CONFIG に調整値)
  planetarium.css   START 画面のタイトルの色だけ
public/tool/sky-today/   (読むだけ)
  sky-scene.js  sky-calc.js  sky-dome.js(loadSkyData)  astronomy.mjs
  stars.json  constellations.json  messier.js
  • ../../sky-today/… は、静的な import で読みます(public の JS を動的な import() で読むと、Astro の dev サーバーで失敗するため)
  • three は ../lib/three-vrm.js(r183)のものを、createSkyScene(THREE, …) に渡します
sky = createSkyScene(THREE, {
  calc: createCalc(Astronomy), data, S, pixelRatio: cb.renderer.getPixelRatio(), decor: true,
  labelScale: CONFIG.labelScale, bodyScale: CONFIG.bodyScale,
  onMode: () => { if (S.bg) scene.background = S.bg; },
});
scene.add(sky.root);
  • S は Sky Today の状態オブジェクトです(lat lon showCons showNames showMW showMeteors sunTrack sunAlt など)。sky.tick が読み書きします
  • 座標は x 東、y 上、z 南(北が −z)です。sky.root.rotation.y を回すと、「正面」の方位が変わります(北 0、東 π/2、南 π、西 −π/2)
  • 空の構築は約 460 ms(デスクトップ)です。「読み込み中」を一度描かせてから始めます

2. 時間

const sim = { t: Date.now(), rateIdx: RATE_LIVE, live: true, headingIdx: 2, placeIdx: 0 };
// CONFIG.rates = [-3600, -600, -60, 1, 60, 600, 3600](実時間 1 秒あたりの、空の秒数)
  • live のときは Date.now() に追従します。それ以外は、毎フレーム dt * rate ぶん進めます
  • 時刻の表示は Intl.DateTimeFormat(timeZone を場所ごとに指定)です
  • 速く送っている間(|rate| ≥ 600)は、太陽・月・惑星の再計算(1 回約 3 ms)を 1 秒に 10 回までにします。sky.tick が毎フレーム再計算するので、呼ぶ前後で S.lastBodies(最後に計算した時刻)を操作して間引きます
const keep = S.lastBodies, now = performance.now(), throttled = fast && now - bodiesAt < 100;
if (throttled) S.lastBodies = t;       // 「いま計算済み」に見せて、再計算を飛ばす
sky.tick(new Date(t), dt);
if (throttled) S.lastBodies = keep; else if (S.lastBodies !== keep) bodiesAt = now;

星の回転は行列なので、間引いても毎フレーム滑らかです。

3. 視線についてくるパネル

タップのメニューは、ゴーグルをつけると押せません。createGazeButton の丸いボタンを 3 段に並べます。

段(俯角)ボタン
1(−20°)<< STOP >> -1か月 +1か月 場所
2(−33°)-1d -1h NOW +1h +1d
3(−46°)星座 名前 銀河 流星 日の道 正面
  • << >> は、速さを 1 段ずつ変えます。STOP は等速に戻します(時刻はそのまま)。NOW はいまの時刻に戻して LIVE にします
  • -1d -1h +1h +1d と ±1 か月は、見つめ続けると 0.35 秒ごとに繰り返します(stepRepeat)
  • 3 段目は、見つめるたびに ON / OFF です。ラベルにいまの状態を出します

向きについてくる

  • 水平から −12°(panelFollowPitch)より上を見ている間、パネルの向き(yaw)は視線についてきます
  • 見下ろすと止まるので、ボタンを順に見つめられます
  • ボタンは、カメラから 2 m の球面に、段の俯角と列の角度で置きます(layoutPanel)

縦長の画面での縮小

PC の縦長の窓では、片目の横の視野が狭く、ボタンが画面からはみ出します。最長の行が横の視野の 8 割に収まる大きさまで、ボタンと間隔を縮めます。横長のスマホでは縮みません。

const hHalf = degrees(Math.atan(Math.tan(radians(camera.fov) / 2) * cb.aspectEye));   // 片目の横の半視野
const maxN = Math.max(...panel.rows.map((r) => r.length));
const k = Math.min(1, (hHalf * CONFIG.panelFit) / ((maxN - 1) / 2 * CONFIG.panelStepDeg + 6.5));

ラベルを小さくする

ボタンの文字の大きさは、行数と最長の行の長さから決まります。前後に空行を足すと、文字が小さくなり、丸の中に収まります。

const small = (a, b) => `\n${a}\n${b}\n`;

4. 星を見つめると名前が出る

レイキャストは使わず、視線の方向と、各天体の方向の角度で選びます。

  • 視線の方向は、sky.root の回転(正面の方位)を戻した、地平座標(x 東、y 上、z 南)にします
  • 候補は、太陽・月・惑星、星雲・星団(夜で、天の川が ON のとき)、星(夜で、pickMaxMag 以下)です
  • 視線の中心から pickDeg(2°)以内で、角度が最も小さいものを選びます。名前のある星、惑星は、少し選ばれやすくしてあります
  • 地平線の下の天体は、候補にしません
const altAz = (v) => ({
  alt: Math.asin(Math.max(-1, Math.min(1, v.y))) * RAD,
  az: ((Math.atan2(v.x, -v.z) * RAD) + 360) % 360,       // 北 0、東 90
});
  • 判定は 10 回/秒です。同じ天体を見つめ始めると、輪が出ます(0.25 秒後)。pickHold(0.8 秒)で、下に情報が出ます。視線を外しても pickKeep(1.5 秒)は残ります
  • 輪が満ちる様子は、cb.gaze.extra(0〜1)に入れると、照準の周りの輪に出ます
  • 時間を速く送っている間(等速より速いとき)と、足元(−10° より下)を見ている間は、判定しません

名前のない星

星のデータ(stars.json)に名前があるのは 29 個だけです。名前がない星は、星座の名前から「ぎょしゃ座の星」と出します。

  • 星座線(constellations.json)の頂点の座標は、星の座標と同じです
  • ra.toFixed(2) + ',' + dec.toFixed(2) をキーに、星座線の頂点から、星 → 星座の Map を作ります
  • 3 等より明るい星は 164 個中 156 個が一致します

表示

シリウス
おおいぬ座  -1.4等  白
南東  高度 30°
  • 色は、B−V 値(colorName(bv))、方角は方位から(dirName(az))出します(どちらも sky-scene.js の関数)
  • 月は、月齢と月相の名前も出します。世界に置いた HUD(createWorldHud)に、画面の縦横比に合わせた大きさで出します

5. 場所

places: [
  { name: '東京',     lat:  35.6812, lon: 139.7671, elev: 0, tz: 'Asia/Tokyo',       tzLabel: 'JST', heading: 2 },
  { name: 'シドニー', lat: -33.8688, lon: 151.2093, elev: 0, tz: 'Australia/Sydney',            heading: 0 },
  { name: 'トロムソ', lat:  69.6492, lon:  18.9553, elev: 0, tz: 'Europe/Oslo',                 heading: 2 },
],
  • 「場所」ボタンを見つめるたびに、順に切り替わります
  • S.lat / S.lon は、毎回の sky.tick で読まれます。値を差し替えて、すぐ tick を呼ぶだけです(空の作り直しは不要)
  • 時刻の表示は、場所の tz の Intl.DateTimeFormat に替えます。tzLabel がない場所は、timeZoneName: 'shortOffset' の GMT+11 を UTC+11 にして出します
  • 「正面」は、その場所の heading にします(シドニーは、北が空の高い側なので、北向きから始めます)
  • 見つめていた星の表示は、消します
function setPlace(i) {
  sim.placeIdx = (i % n + n) % n;
  const p = CONFIG.places[sim.placeIdx];
  S.lat = p.lat; S.lon = p.lon; S.elev = p.elev;
  fmtParts = mkFmt(p.tz);
  pick.key = ''; pick.shown = null; pick.t = 0; pick.acc = 0;
  setHeading(p.heading);
  if (sky) { S.lastBodies = 0; sky.tick(new Date(sim.t), 0); bodiesAt = 0; }
}

6. ±1 か月

同じ日・同じ時刻のまま、月だけを動かします。月末は、移動先の月の最終日に丸めます(1/31 の次は 2/28)。

const bumpMonth = (n) => {
  const d = new Date(sim.t), day = d.getUTCDate();
  d.setUTCDate(1); d.setUTCMonth(d.getUTCMonth() + n);
  d.setUTCDate(Math.min(day, new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 0)).getUTCDate()));
  sim.live = false; sim.t = d.getTime();
};
  • 先に日を 1 にしてから月を動かさないと、setUTCMonth が 3/3 のように溢れます
  • 月は UTC で動かします。夏時間のある場所では、現地の時刻が 1 時間ずれることがあります

7. 太陽の通り道

createSkyScene の sky.setSunPath() は、S.sunTrack(高度・方位の表)の、地平線より上の部分を線にします。表を作って入れ、呼びます。

S.sunTrack = calc.track('Sun', calc.observer(S.lat, S.lon, S.elev), new Date(t - dayInfo.min * 60000), 10);
sky.setSunPath();
  • calc.track(body, obs, start, stepMin) は、start から 24 時間分を、stepMin 分刻みで返します
  • 起点は、場所の現地の 0 時です。時刻の表示で求めた「0 時からの分」を引きます
  • 日付か場所が変わったときだけ作り直します(キーは 場所|年月日)。OFF のときは、S.sunTrack = null で線を消します
  • 季節で線の高さと長さが変わります。トロムソで極夜の日は、地平線より上の点がなく、線は出ません

8. つまずいた点

  • 3 段目が地面に埋まる: 最初は −52° に置きました。カメラの高さ 1.6 m、距離 2 m だと、ボタンの高さが地面(y = 0)に近く、半分が埋まりました。−46° に直しました。段を足すときは、地面に埋まらないか見ます
  • 縦長の窓で、ボタンが大きすぎる: 原因は 2 つありました。横の視野が狭いこと(3 章の縮小)と、ボタンの文字が大きいこと(small() で空行を足して直しました)
  • 1/31 の次が 3/3 になる: 6 章のとおり、日を 1 にしてから月を動かします
  • 日の道が出ない: 最初は OFF にしています(星座を見る邪魔になるため)。CONFIG.sunPath で、最初から出せます

9. 制約

  • 場所は 3 か所の固定です。位置情報は取りません(許可の画面が出るため)
  • 実際の方位(コンパス)には合わせていません。「正面」の方位は、ボタンで決めます
  • 時間を速く送っている間は、星の名前を出しません
  • 星のデータは 5044 個です。名前のある星は 29 個で、あとは星座名で出します

クレジット

  • 星のデータ: d3-celestial(BSD-3)
  • 天体の計算: Astronomy Engine(MIT)
  • 星空の描画: Sky Today(lain-lab.com)

アプリの CREDIT ボタンに、同じ内容を出します。

関連

  • PLAZA(見つめると近づく VRM の広場)
  • Sky Today(今日の空)