やり方
近づくズームと地図のタイル
天体ごとにどこまでズームできて、なぜそこで止まるのか。近づいても細かく見せる地図のタイル (NASA と USGS のパブリックドメインの地図から作り、デモと並べて配信し、パッケージには入れません)、天体がのっぺりしないための汎用的な細かさ、近づくときにかかる負荷を説明します。
painted の見た目の天体の地図は、赤道を一周して2048ピクセルです。ふつうの大きさの絵では、Fit の約2倍までくっきり見え、それより近づくとぼやけます。そのため <wakusei-body> は8倍で止まっていました (<wakusei-portrait> も、タイルが使えるようになるまでは8倍で止まっていました。下を見てください)。衛星の絵では、惑星は広い絵の真ん中の小さな円なので、8倍でも惑星は画面の3分の1ほどの幅しかありません。「どうして土星にこれ以上近づけないの?」という声が出たのは、そのためです。
いまは、天体ごとにその天体自身の細かさが足りなくなるところまでズームでき、その限界を要素が知らせます。限界は天体ごとに違い、二つの仕組みが支えています。
- 細かい地図のタイル。 2048ピクセルの絵より細かい、パブリックドメインの地図がある天体では、見えている部分だけを、最大で一周16384ピクセルまでの地図タイルのピラミッドから天体にのせます。タイルはズームに必要なときだけ取得し、いらなくなったら手放します。
- 汎用的な細かさ。 地図が、どれほど細かくても、画面のピクセルより4分の1以上広く引き伸ばされるところでは、シェーダーがオクターブごとにノイズを重ねて、天体がのっぺりしないようにします。これは天体を実際に測った特徴ではなく、ノイズです。そのことは下で説明します。
天体ごとのズームの限界
ズームは絵の大きさに対する倍率です。ズーム1では、絵は真ん中から端まで extent 天体半径の範囲を映します (天体一つなら2.2、衛星の絵なら、いちばん遠い衛星や輪が届く範囲で、7〜70です)。円盤の幅は、絵の幅の zoom / extent 倍です。赤道を一周 W テクセルの地図を、幅 D CSS ピクセルの円盤にのせると、1テクセルは π D / W ピクセルの幅に見えます。
- 本物の細かさが足りなくなるのは、幅480ピクセルの絵で、1テクセルが 1.6 CSS ピクセルより広くなるところです。汎用的な細かさがない、地図のある天体の限界はここです。
- 汎用的な細かさは、岩や氷の天体を1.15倍、やわらかい天体 (ガス惑星の縞、金星の雲、タイタンのもや) を2倍遠くまで運びます。輪の帯は、サンプルを一列に並べたものなので、1.25倍だけです。土星は、輪の細かさの限界まで行けます。
- どの天体も、上限は8倍を下回らず (陰影つきの見た目がいつももっていた値です)、1000倍を超えません。
結果は zoom = extent × reach です。reach は、円盤が絵の何枚分の幅まで大きくなってよいかです。表は、天体一つの場合 (extent 2.2) と、各惑星の衛星の絵の場合を示します。値は、デモのアドレス (?body=saturn&moons=1 など) で開いたときのものです。
| 天体 | いちばん細かい地図 (一周のテクセル数) | 細かさの出どころ | 単独 | 衛星の絵が届く範囲 | 衛星の絵 |
|---|---|---|---|---|---|
| 太陽 | 2048 | 従来どおり | 8倍 | ||
| 水星 | 8192 | タイル | 22倍 | ||
| 金星 | 2048 | 地図 | 9.6倍 | ||
| 地球 | 16384 | タイル | 44倍 | 69.9 天体半径 | 1000倍 |
| 火星 | 16384 | タイル | 44倍 | 7.6 天体半径 | 151.9倍 |
| 木星 | 2048 | 地図 | 9.6倍 | 29.2 天体半径 | 126.9倍 |
| 土星 | 2048 | 地図 | 36.4倍 | 66.9 天体半径 | 1000倍 |
| 天王星 | 2048 | 地図 | 9.6倍 | 25.2 天体半径 | 109.3倍 |
| 海王星 | 2048 | 地図 | 9.6倍 | 15.8 天体半径 | 68.5倍 |
| 冥王星 | 4096 | タイル | 11倍 | 60.8 天体半径 | 304.1倍 |
| ケレス | 2048 | 地図 | 8倍 | ||
| オルクス | 4096 | シェーダー | 11倍 | 21.7 天体半径 | 108.5倍 |
| ハウメア | 4096 | シェーダー | 11倍 | 72.3 天体半径 | 361.3倍 |
| クワオアー | 4096 | シェーダー | 11倍 | ||
| マケマケ | 4096 | シェーダー | 11倍 | ||
| ゴンゴン | 4096 | シェーダー | 11倍 | ||
| エリス | 4096 | シェーダー | 11倍 | 35.5 天体半径 | 177.3倍 |
| セドナ | 4096 | シェーダー | 11倍 | ||
| 月 | 16384 | タイル | 44倍 | ||
| フォボス | 4096 | シェーダー | 11倍 | ||
| ダイモス | 4096 | シェーダー | 11倍 | ||
| イオ | 8192 | タイル | 22倍 | ||
| エウロパ | 1024 | 地図 | 8倍 | ||
| ガニメデ | 2048 | 地図 | 8倍 | ||
| カリスト | 8192 | タイル | 22倍 | ||
| ミマス | 4096 | タイル | 11倍 | ||
| エンケラドス | 8192 | タイル | 22倍 | ||
| テティス | 8192 | タイル | 22倍 | ||
| ディオネ | 8192 | タイル | 22倍 | ||
| レア | 8192 | タイル | 22倍 | ||
| タイタン | 2048 | 地図 | 9.6倍 | ||
| イアペトゥス | 8192 | タイル | 22倍 | ||
| ミランダ | 4096 | シェーダー | 11倍 | ||
| アリエル | 4096 | シェーダー | 11倍 | ||
| ウンブリエル | 4096 | シェーダー | 11倍 | ||
| ティタニア | 4096 | シェーダー | 11倍 | ||
| オベロン | 4096 | シェーダー | 11倍 | ||
| プロテウス | 4096 | シェーダー | 11倍 | ||
| トリトン | 8192 | タイル | 22倍 | ||
| ネレイド | 4096 | シェーダー | 11倍 | ||
| カロン | 4096 | タイル | 11倍 | ||
| ニクス | 4096 | シェーダー | 11倍 | ||
| ヒドラ | 4096 | シェーダー | 11倍 | ||
| ヴァント | 4096 | シェーダー | 11倍 | ||
| ヒイアカ | 4096 | シェーダー | 11倍 | ||
| ナマカ | 4096 | シェーダー | 11倍 | ||
| ディスノミア | 4096 | シェーダー | 11倍 |
タグの zoom-min と zoom-max は、これらすべてに優先します。タイルの組が見つかった天体は、範囲がタイルの許すところまで広がります (その要素のバーも追いかけます)。タイルの組が見つからなかった天体は、2048ピクセルの絵の許す範囲のままです。
タイル
タイルの組は、天体の地図を、いくつかの大きさで512ピクセルの WebP の正方形に切ったものです。レベル L は幅 2048 × 2^L ピクセルで、2^(L+2) 列、2^(L+1) 行です。0列目は経度 -180、0行目は緯度 90 にあり、タイルは <body>/<level>/<column>/<row>.webp です (<body> は天体、<level> はレベル、<column> は列、<row> は行)。レベル0はパッケージ自身の2048ピクセルの絵で、切りません。manifest.json に、天体ごとに何があるかが書いてあります。
| 天体 | 最上位のレベル | 出典 |
|---|---|---|
| 火星 | 3 (16384) | バイキングの火星デジタル画像モザイク 2.1、カラー、1ピクセル1 km |
| 月 | 3 (16384) | LRO カメラのカラー地図 (NASA SVS、CGI Moon Kit) |
| 地球 | 3 (16384) | 地形と海底地形つきの Blue Marble Next Generation |
| 水星 | 2 (8192) | メッセンジャー MDIS の全球ベースマップ |
| イオ、カリスト | 2 | ガリレオとボイジャーのモザイク (NASA、USGS) |
| レア、エンケラドス、テティス、ディオネ、イアペトゥス | 2 | カッシーニの全球カラー地図 (NASA、JPL、SSI、LPI) |
| トリトン | 2 | ボイジャー2号のモザイク (ポール・シェンク、LPI) |
| 冥王星、カロン、ミマス | 1 (4096) | ニュー・ホライズンズとカッシーニのモザイク |
タイルは、パッケージ自身の2048ピクセルの絵を引き伸ばしたものに、もとの画像の細かい部分を足したものです。もとの画像は、2048ピクセルの絵を作ったときと同じやり方 (同じ回転、同じ帯の投影、同じ彩度の落とし方) で球にのせ、2048ピクセルの絵にない細かさだけを、本当にデータがあるところに足します。だれも撮っていない極の黒い部分には足しません。だからタイルの色、色の調子、埋めた極は、どのレベルでも2048ピクセルの絵のままで、変わるのは鮮明さだけです。もとの画像のライセンスとクレジットは NOTICE.md にあります。
ガス惑星、金星、タイタン、ケレス、ガニメデ、エウロパ、太陽には、パッケージで使える、もっと細かい地図 (パブリックドメインのもの、またはライセンスを引き継げるもの) がないので、タイルはありません。2048ピクセルの絵と汎用的な細かさがその分を支えます。太陽は、いままでと同じように描かれます。
要素がすること
- ズームが初めて2048ピクセルの絵より細かいものを求めたとき、タイルの組を一度だけ読みます (
manifest.json。ページ内のすべての要素で共有します)。タイルが必要になるまで、ほかは何も取得しません。 - カメラの枠と天体の極と本初子午線から、見えている部分を求めます (枠の中に極があれば、すべての経度が見えています)。レベルは、ズームとピクセル比から決めます。テクセルがデバイスピクセルより4分の1を超えて広くならない最小のレベル、なければいちばん細かいレベルです。
- 絵の窓を作ります。 同じレベルのタイルを丸ごと、最大16枚 (2048ピクセル四方、または極のまわりの4096×1024のような帯) 集めて、シェーダーで地図の上にのせます。窓のふちは地図に溶けこみ、窓は経度180をまたげます。極をふくむ窓は、極にふちがありません。16枚より多く必要になる見え方 (幅の広い絵でズームが小さいとき、または極が入っているとき) は、ズームが大きくなるまで2048ピクセルの絵のままです。
- 見え方が落ち着くまで待ってから (140ミリ秒) 取得します。ドラッグやピンチの途中で通りすぎるものは取得しません。同時に取得するのは最大6枚です。見えている範囲が窓の中にあるあいだは、窓を使い続けます。
- 必要なときだけ描きます。 窓の準備ができると、1回の描画を頼みます。ほかの描画といっしょに毎秒30回までに抑えます。止まっているとき、画面の外にあるとき、隠れたタブのときは何も描かず、動きを減らす設定にしている人には、これによる動きは加わりません。
- 持っているものを手放します。 必要がなくなった窓は4秒間 (すぐ戻るかもしれないので) 置いてから、窓の絵と、デコードしたタイルをすべて手放します。使っているあいだに持つデコード済みのタイルは最大48枚で、窓のものが優先です。
- エラーを出さずにもとの絵に戻ります。
tilesのアドレスがない、manifest が読めない、タイルが取得できない場合は、天体は自分の絵で描かれ、取得できなかったタイルは30秒間は取り直しません。element.stats.tilesが、いまの状態 (ready、level、shown、held、loading、failed、requested、pixels) を、element.stats.zoomMaxが範囲を伝えます。
かかる負荷
| グラフィックスメモリ | 窓は一つで、ミップマップをふくめて、1チャンネル8ビットの2048ピクセル四方が最大です。約22 MBで、新しい窓を作る一瞬は、もう一つ分が必要です。パッケージの2048ピクセルの地図は、その下に残ります (これまでどおり約22 MB)。 |
| デコード済みのタイル | 最大48枚 (1枚約1 MB) で、ページのメモリを48 MB使います。窓といっしょに手放します。 |
| ネットワーク | 見えているタイルです。窓は最大16枚、1枚約20〜50 KB です。ズームしないページが取るのは、manifest.json (数 kB) だけです。 |
| キャンバス | 変わりません。キャンバスは絵の大きさで、レンダラーの上限 (1辺4096ピクセル、600万ピクセル) の中です。ズームしても描くピクセルは増えません。 |
| 端末 | メモリが2 GB 以下と答える端末 (navigator.deviceMemory) は、タイルを使いません。 |
タイルの置き場所
タイルは npm パッケージにも、リポジトリにも入っていません。 15の天体で最大16384ピクセルまでだと約77 MB になり、リポジトリはすべてのファイルのすべての版を、ずっと残してしまうからです。タイルは scripts/make-body-tiles.py でもとの画像から作り、Pages のワークフローがビルドして、デモと並べて https://johnmorrisdotca.github.io/wakusei-planets/tiles/ から配信します (GitHub Pages は access-control-allow-origin: * を返します。公開リポジトリでは無料です)。
要素は、何も指定しなければそこを見にいきます。自分で絵を配信するページ (texture-base を指定するページ) は、場所を教えないかぎりタイルを使わないので、頼んでいないページから外へ出ていくことはありません。
<wakusei-body body="mars" look="realistic" texture-base="/pictures/" tiles="/tiles/"></wakusei-body>
<wakusei-body body="mars" look="realistic" tiles="off"></wakusei-body>自分で配信するには、作ってフォルダーをコピーします。
python3 -m venv .cache/venv && .cache/venv/bin/pip install pillow numpy
.cache/venv/bin/python -I scripts/make-body-tiles.py buildpnpm tiles は2行目を実行します。スクリプトは、もとの画像を一度だけ .cache/body-tiles/ (git の対象外) にダウンロードし、SHA-256 がスクリプトに書いたものと違うファイルは使わず、もとの画像が2048ピクセルの絵に重なっているか (明るさの相関を、4分の1回転、半回転、4分の3回転ずらして調べます) を確かめて、.cache/body-tiles-out/ に書き出します (数分かかります)。各レベルは、もとの画像全体から一度に作り、タイルごとには作りません。だから隣のタイルは境目で合い、経度180にも継ぎ目がありません。pnpm test:tiles は、テスト用の地図を切って、ピラミッドの形、各レベルが土台の色ともとの画像の細かさをもつこと、もとの画像にデータのないところに細かさが出ていないこと、タイルが継ぎ目なくつながることを確かめます。
汎用的な細かさ
地図の1テクセルが引き伸ばされ、画面の1ピクセルより4分の1ほど大きくなると、シェーダーは色にバリューノイズを重ねます。重ねるたびに大きさは半分になり、セルが2ピクセル以上あるあいだだけ重ねるので、画面に映らない細かさは出ませんし、ちらつきもしません。岩や氷の天体にはざらつきが、ガス惑星、金星、タイタンには、緯度の線に沿って伸びた、かすかなノイズ (点ではなく筋) が加わります。これはノイズであり、実際に測った特徴ではありません。 同じ種類の天体ならどれでも同じで、クレーターでも嵐でもなく、のせている絵が足りなくなったところにだけ足します。地図が細かい天体ではノイズが少なく、光る天体 (太陽や恒星) には加わらず、ズームしても地図がくっきりしているなら加わりません。
シェーダーだけで描く天体 (illustrated の見た目と、地図のない衛星) は、クレーターや縞をどんな大きさでも数字から描きますが、描ける細かさには限りがあるので、4096ピクセルの地図として数えます。
土星の輪
輪は、カッシーニのモザイクから作った、C環の内側のふちからA環の外側のふちまでの2048個のサンプル (1つ30 km) の帯です。サンプルが1ピクセルの1.25倍より広くなるまではくっきり見え、それ以上では、帯より細かいノイズを半径方向に、重ねるたびに細かくしながら足して、細い輪 (リングレット) にします。帯のすきま (カッシーニの間隙、エンケの間隙)、色、輪への惑星の影、惑星への輪の影は、帯の値と幾何で決まるもので、これまでどおりです。輪のシェーダーは、テクセルが1ピクセルより小さいところでは絵を変えません。
ポートレートと宇宙ビューでは
<wakusei-portrait> と <wakusei-space> は天体をペインターで描くので、同じタイル、同じ窓、同じ表がそのまま使えるようになりました。新しいのは、天体が「カメラに見えている自分の部分」を自分で割り出すことです。ペインターにはカメラが渡され (PaintInfo.view)、画面を格子に分けて通した光線と、手前側の縁に沿った点の輪を、天体自身の座標系で天体に当てます。この計算は、正射影のカメラ (ポートレート) にも、透視投影のカメラ (宇宙ビュー) にも、画面の真ん中にある天体にも、脇にある天体にも、同じ一つの計算で済みます。カメラを渡さない呼び出し側 (chikyu-earth の宇宙ビューなど) は、これまでどおり天体自身の絵を受け取ります。
- 遠いビューでは何も取りに行きません。 半径が150デバイスピクセルに満たない天体は調べません。タイルのセット (
manifest.json) も、2048ピクセルの地図ではカメラに見えているものの細かさが足りなくなったときに、はじめて読みます。近づかないポートレートや宇宙ビューは、タイルのセットもタイルも読みません。 - ポートレートは、いちばん遠くまで届く天体の限界までズームできます。 天体ごとに、天体単独のときと同じ測り方をします。Fit のとき、ビューの短い辺の真ん中から端までに絵が何半径ぶん入るか (
extent)、持っている中でいちばん細かい地図 (タイルが見つかれば最も細かいレベル)、表面がやわらかいかどうか、の三つです。範囲は、天体ごとの限界のうちいちばん大きいもの (ただし8倍以上) です。地球だけのシーンは地球の限界まで、惑星の横に衛星があるシーンはその衛星の限界までズームできます。細かい地図のない天体だけのシーンは8倍のままです。線画のポートレートは8倍のままなので、シーンの見た目を切り替えても範囲は変わりません。タイルが見つかる前の範囲は、これまでどおり8倍です。 - 宇宙ビューの上限は3倍のままで、変わるのは3倍で見える絵です。 カメラは表面から1.05半径より近づかず、ズーム3では天体の中心から1.13半径、つまり地表から0.13半径の高さにいます。そこから視野角35度で見えるのは、幅が半径の約8パーセント (地球なら約530 km) の範囲です。いちばん細かいレベル (一周16384ピクセル) のテクセルは、高さ420ピクセルのビューでおよそ2ピクセル幅になります。上の表で地球・火星・月の細かさが足りなくなるとされているのは、ちょうどそこです。ですから、表から見ても宇宙ビューにはこれ以上進める先がなく、カメラもこれ以上は近づけません。これまでは2048ピクセルの地図を8倍に引き伸ばしていたものが、いまはレベル3のタイルが等倍で、デモのような横長のビューなら4〜9枚、正方形のビューならそれより少ない枚数が表示されます。タイルを使うのは、天体を地図で描くときです。地球はどちらの見た目でも、火星、月などは
paintedの見た目のときです (宇宙ビューのrealisticはシェーダーで描くので、タイルはありません)。タイルのある水星、冥王星、衛星は、持っているもの (粗いレベル、やわらかい絵) がそのまま出ます。木星、金星、タイタンなど、細かい地図のない天体は、これまでと同じように3倍でやわらかいままです。 - 窓がつくのは、カメラが近づいている天体だけです。 窓は最大16枚で、グラフィックスメモリは約22 MB です (上のとおり)。宇宙ビューのほかの天体は、点か、150ピクセルよりずっと小さい円なので、窓はつきません。ビューから外れた天体は、4秒後に窓を手放します。
- 窓は自転に合わせて替わります。 時間とともに回る天体 (宇宙ビューで1秒が1日のとき) は、窓の下で動きます。見えている部分が窓の中にあるあいだは窓をそのまま使い、外に出たら、ビューが140ミリ秒落ち着いてから窓を取り替えます。タイルはどれも一度だけ取り、取れなかったタイルは30秒間は取り直しません。
ポートレートと宇宙ビューの element.stats.tiles は、それぞれの天体の窓がしていることを合計して返します (requested、loading、failed、そしていま天体に載っているいちばん細かいレベルの shown。なければ null)。ポートレートの element.stats.zoomMax は、いまの範囲です。自分の3Dシーンでペインターを使うページは、info.view でカメラを渡し、createPainter({ tiles }) でタイルの場所を伝えます。自分の three.js シーンで使う を見てください。
変わらないもの
- 移動は天体を絵の中に留めます (絵の真ん中は、天体の縁から半径の半分までしか外に出られません)。そのため、深いズームでは、円盤の外側にはみ出た輪の部分を移動で真ん中に持ってくることはできません。円盤を横切る部分は持ってこられます。
- レンダラーの上限 (一辺4096ピクセル、600万ピクセル)、描くのは必要なときだけで毎秒30フレームまで、見えていないときは何も描かないこと。
pnpm test:freezeが同じ上限で同じことを確かめます。 <wakusei-space>の範囲は、いまも100万分の1倍〜3倍で、カメラはいまも表面から1.05半径より近づきません。
この日本語は、まだ日本語を母語とする方の確認を受けていません。訂正を歓迎します。