[Astro] #190 トップページの空を「今日の空」の計算に置き換える — 星・太陽・月と、視点の切り替え
はじめに
前に、日課アプリとして「今日の空」を作りました。場所と日付から、太陽・月・星の位置を計算して、全天の 3D で見せるアプリです。
[Astro] #186 今日の空 — 太陽・月・星を計算して、全天 3D と Quest の VR で見る日課アプリ
場所と日付から、太陽と月の時刻・位置、星空を計算して見せる、ブラウザだけで動くアプリ「今日の空」を作りました。
lain-lab.comトップページの背景は、これまで、スカイボックスの絵(テクスチャ)を貼っているだけでした。そこで今回は、この背景の空を、「今日の空」の計算に置き換えました。
[Astro] #185 トップページの世界に風・雪・夜を足す 01 — 髪・霧・雪を 1 つの風でつなぎ、調整パネルで決める
トップページの 3D の世界に、風、雪、時間帯(夜)、スポットライトを足しました。
lain-lab.comこの記事は、その実装のまとめです。
変えたこと
- 右クリックメニュー(SKY)に、スカイボックスの項目「Sky Today」を足しました。選ぶと、背景が、いまの時刻・場所の空になります
- 星・天の川・太陽・月・惑星の計算は、「今日の空」のコードをそのまま使います(
public/tool/sky-today/) - SKY メニューに、星座線・星の名前・天の川・流れ星の入切と、場所の選択を足しました
- 固定だったカメラに、視点を 3 つ足しました(DEFAULT / DOME / SKY)。メニューの
CAMERA、キー、ターミナルのコマンドで切り替えます - 時間帯(夕方・夜)の「空の色の掛け算」は、Sky Today のあいだは止めます
視点変更
動画(PC)
動画(WebXR/VR)
方針: 空を差し替えて、光は元の絵に任せる
トップページの空(WiredSkybox)は、半径 500 の球に絵を貼ったものです。同時に、その絵を環境マップ(<Environment map>)としても使っています。ステージは金属の素材なので、まわりの光を映す環境マップがないと、真っ黒になります。
そこで、次のように分けました。
- 見える空: Sky Today の計算で描く
- 光(環境マップ): 今までの絵に任せる
スカイボックスの一覧(skybox.json)には、新しい項目を足しました。path は、環境マップ用の絵のまま残します。live に sky-today と書くと、見える空だけが差し替わります。VR のメニューや MR の側も、この一覧を読むので、path が有効な項目にしておきました。
{
"id": "sky_today",
"name": "Sky Today",
"path": "(環境マップに使う、既存の絵)",
"live": "sky-today"
}
WiredSkybox には background の指定を足しました。false のときは、空の球を出さず、環境マップだけを置きます。
<WiredSkybox
rotationSpeed={currentStageData.rotationSpeed}
data={currentSkyboxData}
background={(currentSkyboxData as any).live !== 'sky-today'}
/>
{(currentSkyboxData as any).live === 'sky-today' && <WiredSkyToday />}
空を組み立てる
新しい部品 WiredSkyToday.tsx が、空を作ります。
「今日の空」のコードを読み込む
「今日の空」の本体(sky-scene.js)は、public の下にある素の JavaScript です。これを import() で読むと、Vite の開発サーバーで失敗します。そこで、planetarium-loader.js を <script type="module"> で読み込み、部品を window.__skyToday に入れる形にしました。VR のプラネタリウム(WiredPlanetarium.tsx)と同じ方法です。
空の組み立ては、次の関数です。
createSkyScene(THREE, { calc, data, S, pixelRatio, onMode, decor, labelScale, bodyScale })
S は、場所・時刻・表示の入切を持つ状態です。毎フレーム、tick(new Date(), dt) で、いまの時刻の空に更新します。時刻は、PC の時計です。
decor、labelScale、bodyScale は、今回、足した引数です。既定値は、それぞれ true、1、1 で、「今日の空」のページと VR のプラネタリウムの見た目は、変わりません。
decor: false… 地面の円盤、緑の輪、東西南北の文字を出さない(トップページには、ステージがあるため)
カメラについてくる
空は、無限に遠い背景に見えるべきです。そこで、空の root を、毎フレーム、カメラの位置に合わせます。地平線は、カメラの高さになります。微調整のために、weather.json に、次の値を置きました。
yOffset… 地平線の高さの調整(メートル)yawDeg… 空を回す角度。0 のとき、初期のカメラの正面が北
座標は、x が東、y が上、z が南です。カメラの初期の向きは、-z(北)です。
地平線の下を隠す
「今日の空」の空は、地平線の下も、星を描きます。ページでは、地面の円盤が隠していました。トップページでは、地面を出さないので、地平線の下の星が、ステージの隙間から見えてしまいます。
そこで、下半分の半球(半径 90)を、「ふた」として置きました。星・天体は半径 95〜100 にあるので、その内側です。
- 色は、空の地平線の色(
domeMatのuHor)を共有します。地平線の色が変わると、ふたも同じ色になります - 真下に向かって、少しずつ暗くします(
capDark、0 で暗くしない)
float k = clamp(-normalize(vP).y, 0.0, 1.0);
gl_FragColor = vec4(uHor * (1.0 - uDark * k), 1.0);
色を合わせる
最初に動かしたとき、色が、「今日の空」のページと違いました。トップページの描画は、ACES のトーンマッピングが掛かる設定です。「今日の空」のページは、掛けていません。
そこで、空の root の中の素材を、すべて toneMapped = false にしました。空だけが、ページと同じ色になります。ステージとアバターは、今までどおりです。
文字と天体の大きさ
星座名・星の名前・星雲の名前は、文字を貼った板(スプライト)で、世界座標での大きさが、固定です。「今日の空」のページと VR は、画角が広いので、ちょうどいい大きさでした。トップページのカメラは、画角が 35° と狭く、同じ大きさでも、画面では大きく見えます。最初の画面では、文字が画面を覆うほど大きくなりました。
そこで、倍率を 2 つ足しました。
labelScale… 星座名・星の名前・天体の名前bodyScale… 太陽・月・惑星の丸
トップページの値は、weather.json の skyToday に置きます。初期値は、目で見て、labelScale が 0.4、bodyScale が 0.5 にしました。
SKY メニューの設定
右クリックメニューの SKY タブで、Sky Today を選ぶと、次の項目が出ます。
- STAR_LINES(星座線と星座名)、STAR_NAMES(星の名前)、MILKY_WAY(天の川・星雲・星団)、METEORS(流れ星)
- PLACE …
SKY_TODAY(「今日の空」のページで、最後に使った場所)またはTOKYO
設定は、localStorage の wired_sky_today に保存します。再読み込みしても、残ります。初期値は、weather.json の skyToday の showCons、showNames、showMW、showMeteors で決められます。一度メニューで切り替えた項目は、保存した値が優先です。
場所の SKY_TODAY は、「今日の空」のページが sky-today.v1 に保存した場所を読みます。保存がなければ、東京です。場所を切り替えると、太陽・月・惑星と、空の色を、新しい場所で計算し直します。
視点の切り替え
トップページのカメラは、ずっと固定でした(位置 [0, 0, 4]、画角 35°)。マウスは、アバターの視線と右クリックに使っていて、カメラを動かす操作がありません。
自由に動かす代わりに、視点をいくつか決めておいて、切り替えることにしました。WiredCameraRig.tsx を新しく作り、ROOM のときだけ置きます。
| 視点 | 内容 |
|---|---|
| DEFAULT | 今までの視点(初期のカメラそのもの) |
| DOME | 少し遠くから、斜め下に見下ろす |
| SKY | 空を見上げる(画角を広げる) |
視点は、position(カメラの位置)、target(見つめる点)、fov(画角)で決めます。切り替えると、約 0.8 秒で、なめらかに動きます。着いたら、カメラに触るのをやめて、ほかの処理に任せます。VR / MR の最中は、何もしません。ROOM を出るときは、初期の視点に戻します。
切り替え方
- メニュー(右クリック)の
[<] CAMERA [>]。タブの上にあり、どのタブでも使えます - Alt+1〜9 … 並んだ順の 1〜9 番目。ターミナルに文字を打っている最中でも効きます
- ターミナルのコマンド
view [名前 | next | prev | 番号]
ファンクションキーは、使いませんでした。常駐アプリが、グローバルなホットキーとして使っていることがあるためです。ターミナルの入力欄は autoFocus で、ほぼいつもフォーカスされているので、文字のキーも使えません。そこで、Alt と数字の組み合わせにしました。キーは、weather.json の camera.keys で変えられます。
視点を増やす
視点の一覧は、weather.json の camera.views の並びで決まります。名前を足すと、メニューの CAMERA に、並んだ順で出ます。コードは触りません。
"camera": {
"seconds": 0.8,
"keys": { "sky": "Alt+3" },
"views": {
"dome": { "position": [5, 4, 9.5], "target": [0, -0.4, 0], "fov": 35 },
"sky": { "position": [0, 0, 4], "target": [0, 4, 1], "fov": 60 }
}
}
メニューとカメラの部品は、小さな共有部分(utils/cameraView.ts)でつなぎました。カメラ側が、視点の一覧といまの視点を知らせ、メニュー側が、切り替えの依頼を送ります。ROOM でないときは、一覧が空になるので、CAMERA の行が出ません。
値は、コンソールで決める
位置と向きは、見ながら決めるものです。再読み込みなしで試せるように、コンソールに __camTune を出しました。
__camTune.stage() // ステージの大きさと中心
__camTune.views.dome.position = [6, 5, 11] // 視点を書き換える
__camTune.go('dome') // その視点へ
__camTune.dump() // weather.json に貼れる形で出す
stage() で、ステージ(GLB)の大きさを出すと、約 27.84 × 20.89 × 28.04、中心は [0, -1.2, -1] でした。カメラは、この大きなステージの内側にいます。
時間帯の色との重なり
トップページには、時間帯(朝・昼・夕・夜)の仕組み(WiredTimeOfDay)があります。そのうちの 1 つが、空の色の掛け算です。カメラを中心にした球殻が、背景の色に、時間帯の色(夕方の橙、夜の青)を掛けます。
「今日の空」の空は、時刻と場所から、夕焼けや夜の色を、自分で計算しています。そこに、時間帯の色が掛かると、二重になります。夕方に、空がピンクがかったグレーに見えたのは、これだと推測しています(確認は、これからです)。
そこで、「いま空が Sky Today か」を示す印(skyTodayRuntime.active)を作りました。実行中だけの印で、保存しません。WiredSkyToday が、空ができたら立て、片づけるときに下ろします。WiredTimeOfDay は、印が立っているあいだ、掛け算の殻を出しません。
const tinted = !xr && !skyTodayRuntime.active
&& (cur.sky[0] < 0.997 || cur.sky[1] < 0.997 || cur.sky[2] < 0.997);
画面全体の明るさ、環境光、アバターのスポットライト、環境マップの強さは、今までどおり、時刻で動きます。
詰まったこと
Cannot read properties of null (reading 'add')。空を置く場所に、refを使っていました。同じ場所のWiredSkyboxが、Suspenseで止まっているあいだ、兄弟のrefが外れて、nullになります。自分で作ったGroupをuseMemoで持ち、<primitive object={group} />で置く形に変えて、直りました- 視点を切り替えると、目標と逆を向きました。向きの計算に、カメラではない
Object3DのlookAtを使っていました。カメラは-Zが前ですが、カメラ以外のオブジェクトのlookAtは、+Zが目標を向きます。結果、DEFAULT で空と円柱が見え、DOME では、アバターが見えませんでした。同じ位置で、前方と目標の向きの内積を比べると、−1(逆) でした。Matrix4.lookAt(eye, target, up)から、クォータニオンを作る形にして、+1 になりました - 霧が OFF なのに、再読み込みで出ました。切り替えのイベントが、シーンが受け取る前に送られていました。状態の初期値で、保存した値を読む形に直しました
publicの JS をimport()すると、Vite の開発サーバーで失敗します。<script type="module">で読む形にしました- 文字が大きすぎました。画角の違いを、考えていませんでした。倍率を足して、
weather.jsonに置きました - ブラウザの機能と、重なるキー。F キーを、切り替えに割り当てかけました。常駐アプリのホットキーと重なるので、Alt と数字にしました
未確認のこと・制限
- メニューの
CAMERAの行と、時間帯の色を止める変更は、この記事を書いた時点では、実機で確認していません。夜は__todTune.override = "22:30"、夕方は"17:45"で、時刻を固定して、確かめる予定です - 掛け算の殻は、半径 8 の球です。ステージは約 28 の大きさなので、8 より遠い部品も、夜に暗くなっていた可能性があります。止めると、夜に遠くの機械が、明るく見えるかもしれません(構造からの推測です)
- 太陽の高度で、環境光とスポットライトを動かす連動は、していません。いまは、時刻だけで決めています
- VR のメニューや MR で、新しいスカイボックスの項目が、正しく動くかは、確認していません
- 視点の値(位置・向き・画角)は、見ながら決めた値です。ステージの大きさや、カメラの向きに合わせて、調整します
- 実際の夜空との見比べは、まだです。確認したのは、PC の Chrome です
おわりに
次は、2 つ考えています。
-
太陽の高度で、環境光とスポットライトを動かす。いまは、時刻だけで、決めています
-
VR のメニューと MR での、新しいスカイボックスの確認
Sky Today(日の出・日の入り・月・星を場所と日付から計算)
Lainの世界観で作る学習・実験場。
lain-lab.com