ブラウザに本物の夜空を描く第5節/全10節
大気と星の描画
物理的な大気散乱モデルをブラウザに載せたときに起きた「正午の空が真っ黒」事件と、点光源にすぎない星を肉眼の見えかたに合わせて描くための計算則。
この節が答える問いは2つです。物理的に正しい太陽の光が大気中の分子や粒子にぶつかって、あらゆる向きへ散らばる現象です。青い光ほど強く散らばるので昼の空は青く、低い太陽の光は長い距離の大気を通るので朝夕は赤くなります。のモデルをブラウザに持ち込むと、なぜ最初の画面は真っ黒になるのか。そして、広がりを持たない点光源である星を、肉眼で見た夜空らしく描くとはどういう計算なのか。
空を描いているのは自前のコードではない
空の色は@takram/three-atmosphereというライブラリが描いています。大気の分子による波長依存の散乱(空が青い理由)と、エアロゾルによる散乱(太陽の周りが白くにじむ理由)を事前に積分した毎回計算し直す代わりに、あらかじめ計算した値を並べておいて引くための表です。GPU上では画像データの形で持ち、重い計算を1回の読み出しに置き換えます。を持ち、観測者の位置と太陽の方向から空のある面から、ある向きへ出ていく光の強さを表す物理量です。カメラや目が受け取る明るさに直結するので、物理にもとづく描画はこの値を計算します。値に上限はなく、昼の空では、画面に出すときに使う0から1の範囲をはるかに超えます。を返します。空の色については、太陽が地平線の下にありながら、空がまだ明るい時間帯です。太陽が沈むほど深くなり、夕方は高度−6度までの市民薄明、−12度までの航海薄明、−18度までの天文薄明の順に進みます。朝は逆に、天文薄明から航海薄明、市民薄明をたどって日の出に至ります。の色も地平線付近の減光も、Asterarium側に色の式が1行もありません。
このライブラリはAsterariumを3点で拘束します。描画バックエンドの選択、座標系、そして実際の比のまま持っている広い明るさの範囲を、画面が出せる狭い範囲へ写し取る処理です。どんな曲線で写すかが、色味と明るい部分の印象を決めます。の扱いです。GPU(絵を描くための専用チップ)の上で走る小さなプログラムで、頂点をどこへ置くか、画素をどんな色にするかを計算します。1枚の絵を作るあいだに、頂点や画素の数だけ繰り返し実行されます。がWebGLのシェーダー(GPU上で走る小さなプログラム)を書くための言語で、C言語に似た文法とベクトル演算を備えています。専用なので、ブラウザで3Dを描くWebGLを扱いやすくする、定番のJavaScriptライブラリです。シーン、カメラ、形状、材質といった概念で場面を組み立て、描画命令の細部を引き受けてくれます。の新しいWebGPUバックエンドでは動きません。座標系が地球固定の直交座標なので、観測者を原点とするAsterariumの3Dシーンで物の位置を表す座標系で、1単位がおよそ1メートル、原点は観測者、軸の取り方はNUE(North-Up-East、北・上・東)です。ここでの距離は実際の距離ではなく描画のための便宜的な値で、星は半径1000(約1km)の球面に、太陽・月・惑星はその外側の200万(約2,000km)に置きます。描かれる大気の層の厚さは約6万単位(60km)です。との橋渡しが要ります(その変換数を縦横に並べた表で、3Dでは回転や移動といった座標の変換をひとまとめに表すのに使います。変換を続けて掛け合わせれば、いくつもの段階を1つの行列にまとめられます。は「座標系とシーンの姿勢」の節にあります)。そして材質、つまりthree.jsで「その物体をどう描くか」をまとめた設定がtoneMapped: falseで、生の明るさを0から1の範囲に押し込めず、実際の比のまま扱う描画のやり方です。太陽と暗い星のように何桁も違う明るさを、1枚の絵の中で共存させられます。放射輝度を出し、レンダラー側のトーンマッピングを迂回します。このトーンマッピングを迂回する3つめが事件の発端でした。将来の乗り換えに備え、このライブラリを直接呼ぶのは大気を組み立てるコンポーネント1つだけです。
参照テーブルは自分で配信しています。ライブラリの既定値はGitHubの生ファイル配信を指していて可用性もCORSも遅延も本番向きではないので、4つの明るさを0から1に押し込めずに、実際の比のまま保存できる画像形式です。映画やCGの制作で標準的に使われ、正式にはOpenEXRといいます。画像、合計約7.7 MBを自分の側から配り、ロード済みのテクスチャとして渡しています(URL文字列を渡すとライブラリがGitHubへ取りに行きます)。この4枚は、大きなデータを別ドメインのオブジェクトストレージへ逃がす仕組み(「配信基盤と独立ページ」の節)ではなく、ページ本体と同じオリジンにあります。自前で配れるのは、この連載の軸であるサーバー側で動くコードを持たない作りと矛盾しないからです。EXRは置けばそのまま配れる静的ファイルで、ページ本体と同じ配信の仕組みに何も足さずに乗ります。
付属の星の描画は使っていません。付属データの約9,100星(イェール輝星星表に由来)を同じサイズ・同じ強度の点として置くだけで、星ごとのサイズも色も、星座線も天の川もラベルも無いからです。ただし初期の試作では意図的に有効なままにし、付属の星を天体の座標がいつ時点のものかを示す基準時刻、すなわち元期の一つで、世界時2000年1月1日正午を指します。星のカタログの座標は、多くがこの元期で書かれています。から地球固定系へ回す処理を、このアプリの天文学で、星の見える向きを表すために考える、観測者を中心とした仮想の球面です。距離は持たず、星までの本当の距離は捨てて、どの向きに見えるかだけを扱います。この記事では、その天球を表す3Dシーンの入れ物も同じ名前で呼び、シーンの1単位が約1メートルにあたるので、半径1000、およそ1kmの球面に星を置いています。が同じ向きに回るかの答え合わせに使いました。
「正午の空が真っ黒」事件
先に断っておきます。これは原因を特定して直した話ではありません。画面が真っ黒になった機序はいまも分かっていません。分かったのは、正しく映る構成のほうです。以下は、その構成を見つけて固定するまでの経緯です。
最初の試作は、もっとも素直な構成でした。ライブラリの空をそのまま置き、トーンマッピングはthree.jsのレンダラー標準の明るさを画面の範囲へ写し取る曲線の1つで、明るい部分の色が白へ寄っていく過程をなだらかにします。以前から使われてきた曲線より、写真フィルムに近い階調になります。設定に任せる。結果は、最も明るくなるはずの条件で真っ黒でした。その条件とは観測者の地平線から測った、天体の上向きの角度です。0度が地平線、90度が真上の天頂で、負の値は地平線の下にあることを意味します。75°の正午の太陽で、東京の夏至の正午の太陽の高度(約78°)に近い高さです。
ここまでは順を追えます。ライブラリの材質は放射輝度をそのまま出力し、日中の空の値は1.0を大きく超えます。材質がtoneMapped: falseなので、three.jsがシェーダーへ自動で差し込むトーンマッピングが入らず、レンダラー側のAgX設定は最初から効いていません。行き先は画面用の8ビットバッファ、つまり各色を0から255の整数で持つ領域で、そこが表せるのは0から1に収めた値だけです。圧縮の段をどこも通らない放射輝度をそこへ書けば、明暗の比は失われます。
ここまでの機構から予想されるのは、大きな値が頭打ちになった白飛びの画面です。実際に出たのは真っ黒でした。この事件の最初に断った、機序の分からない食い違いとはこれです。分かったのは、露出、つまりトーンマッピングに入る前に放射輝度へ掛ける倍率をどう動かしても画が1画面を細かく区切った1つ1つの点で、絵はその集まりでできています。色はこの単位で決まるので、描画の重さも塗るべき画素の数でおおよそ決まります。も変わらなかったこと、すなわちトーンマッピングの段にそもそも入っていなかったことです。これはtoneMapped: falseという設定からの推論ではなく、露出を実際に動かして確かめた事実です。動かしたのは、開発中の調整用にURLの?exp=で上書きできる倍率と同じものです。
動く構成は4つの部品が揃って初めて成立します。次のコードは3D描画ライブラリthree.jsの場面を、Reactの部品として書けるようにするライブラリです。ReactがHTMLの要素ではなく3Dの物体を組み立て直します。とpostprocessingライブラリの部品をJSXで並べたものです。4つのうちCanvasのflatだけは、この抜粋には出てきません。Canvasは空全体を載せる描画面で、ここに写した合成のコードより一つ外側にあるからです。以下に挙げる4つは、正しく動く構成での各部品の役割です。
<EffectComposer frameBufferType={HalfFloatType}>
<AerialPerspective
sky={sky}
sun={sky}
moon={false}
sunLight={false}
skyLight={false}
transmittance={sky}
inscatter={sky}
/>
<ToneMapping mode={ToneMappingMode.AGX} />
</EffectComposer>- 中間バッファを16ビット浮動小数点にする
frameBufferType。なければ放射輝度がトーンマッピングの前に1.0で切り落とされ、昼の空も星の核も頭打ちになって、白く飛ぶための余力が消えます。 - 大気を場面を一度描いた画像に対して、あとから画面全体へ掛ける処理です。明るさを画面の出せる範囲へ写し取る処理や、色の調整がこの段で行われます。この記事では、空そのものもこの段で描かれます。として描く
AerialPerspective。既に描かれている内容へ、透過率(奥からの光がどれだけ減って届くか)と散乱入射(視線の途中で大気が加える光)を適用するので、薄明で星が自然に減光する挙動が星側に1行も書かずに得られます。これは大気の物理による減光で、星のシェーダーが自分で掛けるAsterariumで、空がどれだけ夜になっているかを0から1で表す値です。太陽の高度が0度のとき0、空が完全に暗くなる−18度で1になり、星・星座線・天の川・深宇宙天体の印の見え方をまとめて決めます。月は不透明な円盤のまま、その明るさをこの値を0.25から1の範囲に縮めた値で決めるので昼でも薄く見え、惑星は太陽高度についての別のしきい値で明るいうちから現れます。とは別物です。この大気パスが無ければ空は塗られず、正午でも背景は暗いままです。 - HDRを表示可能な範囲へ圧縮する
ToneMappingのAgX指定。toneMapped: falseの相方です。これが無い構成が壊れることは確かで、そのときの画面が真っ黒でした。 - three.jsの描画面をReactの部品にした
Canvasにつけるflat。レンダラー自身のトーンマッピングを無効にします。直前で圧縮済みなので、掛け直せば二重にかかり、明るい部分のコントラストが下がります。
右側のskyは、大気があるかどうかを表す真偽値の変数です。空・太陽ディスク・透過率・散乱入射という4つの属性に同じ変数を渡し、大気のある地上の視点と、大気を切った宇宙からの視点をひとつのつまみで切り替えます。後者は、メニューの「表示設定」にある「大気」の切り替えで大気の描画だけを止めた状態で、観測者の位置は変わりません。sunがskyに従うのは、物理的に正しい太陽ディスクをライブラリに描かせ、自前のコードはその上に非常に明るい光源のまわりに広がって見える、まぶしさのにじみです。目やレンズの中で光が散ることで生じ、光源そのものより大きく広がります。だけを足すためです。moonだけ常に切るのは、自前の月と二重になるからです。大気を切ったときは背景を暗色で塗り、夜係数を1に固定します。背景が暗く星が最大の明るさになるので正午でも星が見え、トーンマッピングは残るので明るさの階調も保たれます。
sunLightとskyLightは、太陽光と空の光を場面の物体に当てる照明です。照らすべき地面や建物のモデルがありません。しかも照明の計算には面の向き、つまり法線が要り、ライブラリはそれを画面全体ぶん集める描画を1回余分に走らせます。ないものを照らすために描画が1回増えるので、どちらも常に切ってあります。
星の描画則 — 明るさは円盤の面積ではない
「明るい星ほど大きな円を描く」という素朴な実装は、物理的にも知覚的にも誤りです。星は点光源で、肉眼では角度的な広がりを持ちません。それでもシリウスが「大きく」見えるのは、大気の揺らぎと、眼の光学系が明るい点を広げる結果です。眼が受け取るのは、明るく飽和した小さな核と、その周囲の淡いにじみです。追加の光量は面積ではなく輝度へ回します。
先に、星の描きかたが2系統あることを言っておきます。ふつうの星は点の集合として、明るい星は常にカメラのほうを向く四角い板、つまりカメラの方をいつも向くように回される、四角形1枚のメッシュです。大きさを自分で決められるので、GPUが点として描く方式より大きく広げられ、光のにじみのような広がりを表せます。として描きます。どこで分けるか、なぜ分けるかはこの後の「2系統の排他描画と、調整用のつまみ」で述べます。この法則は、点として描く天球、ビルボードで描く明るい星、惑星の3か所で使われます。境界の天体の明るさを表す数値で、小さいほど明るく、1等級の差が約2.512倍の明るさにあたります。肉眼で見える限界は、暗い空でおよそ6等から6.5等です。を変えて同じ星が点からビルボードへ移るときや、星と惑星が隣り合ったときに、法則が違えば段差が出るからです。そのためGLSLの本文と既定値は、three.jsもReactも参照しない文字列と数値だけの共有モジュール1つにまとめてあります。恒星間フライトのページ、つまり実際の距離に置いた星のあいだを飛ぶAsterariumで、本編の空とも互いとも3Dシーンのコードを共有しない別ページ群のことで、全部で8つあります(月球儀、月面ウォーク、火星儀、ディープフィールド、オーラリー、恒星間フライト、日食と月食、投影機)。それぞれが専用のコードの塊として配られるので、開いたページの分しかダウンロードされません。も、これだけは読みます。ページどうしでJavaScriptを共有しない決まりがあります。8つある独立3Dページが互いのコードまで背負って、読み込みが重くならないようにするためです。この共有モジュールはthree.jsを含まないので、その決まりが認める例外の範囲内です(決まりと例外、どのページがどう分かれているかは「配信基盤と独立ページ」の節にあります)。コピーを2つ置いても規則は守れます。それでも1つにしてあるのは、2つあればいつか値がずれ、同じ星空が別物になるからです。次に載せるのがそのGLSLの抜粋です。
float starSizePx(float intensity, float K2, float fovFactor, float dpr) {
return K2 * pow(intensity, uSizeExponent) * fovFactor * dpr;
}
float starCoreHDR(float intensity) {
return clamp(intensity, 1.0, uCoreMax);
}
float starPSF(float r) {
float core = exp(-r * r * 26.0);
float r0 = 0.10;
float rr = max(r, r0) / r0;
float skirt = uSkirtGain / pow(rr, uSkirtPow);
float psf = core + skirt;
psf *= 1.0 - smoothstep(0.75, 1.0, r);
return psf;
}星の大きさと明るさの式
3つの関数の値は掛け合わされて1画素になります。画素の色=星の色×核の輝度×点である光源が、レンズや目を通したときにどれだけにじんだ像になるかを表す関数です。星が点ではなく小さなにじみとして見えるのは、この効果によります。の値×星ごとの色に添えて持つ不透明度の値で、0が完全な透明、1が完全な不透明を意味します。×夜係数で、出力のアルファは後ろの3つ、つまり点像分布関数の値と星ごとのアルファと夜係数の積です。ブレンドは加算です。星の色はカタログが持つ青いフィルタを通して測った明るさと、黄色いフィルタを通して測った明るさの差です。値が大きいほど赤い星、小さいほど青い星で、星の表面温度の目安になります。から作った色の値が光の量にそのまま比例する表し方です。画面へ渡すsRGBは人の目の感じ方に合わせて曲げてあるので、光を足したり掛けたりする計算は線形に直してから行い、最後に戻します。で、その作りかたは「データパイプライン」の節にあります。加算なので出力のアルファが合成の重みになることはありません。使い道は判定で、この値が0.003を下回る画素はdiscardして深度も書かせません。深度を書いてしまうと、その画素は空を塗る大気のパスから守られ、昼の空に黒い穴が残るからです。経緯は「深度の規約と、踏んだ不具合」の節にあります。星ごとのアルファには、3つの因子が掛け合わされています。1画素を下回る星のちらつきを抑えるサブピクセル補正、空の明るさに応じて暗い星を消す光害、そして瞬きです。いずれもこの後で詳しく述べます。
実装の細部です。式に出てくるuSizeExponentやuSkirtPowは、外から渡すシェーダー(GPU上で走る小さなプログラム)へ渡す値のうち、1回の描画のあいだ変わらないものです。現在時刻や太陽の方向のように、外から送る設定に使います。です。K2だけは関数の引数で、値はuniformのuSizeKから渡ります。以下ではこのゲインを、uniformの名前uSizeKのほうで呼びます。既定値は6つ。サイズのゲインuSizeKが3.0、指数uSizeExponentが0.25、核の上限uCoreMaxが12.0、裾のゲインuSkirtGainが0.14、裾の指数uSkirtPowが2.6、そして明るい星に足すきらめきの量が0.25です。きらめきの量だけは上の抜粋に出てきません。掛けているのは、明るい星をビルボードで描くシェーダーの側で、共有している抜粋の外だからです。残る2つの因子は別々の役目を持ちます。dprは端末の画素密度の比で、高精細な画面でも星が細らないよう掛けます。fovFactorは視野が狭いほど大きくなる比で、視野60°を1とします。描く大きさが画素で決まる以上、これが無ければズームするほど星は場面に対して小さくなるので、それを打ち消しています。保っているのは画面上の見かけの大きさで、星に角度的な大きさがあるという主張ではありません。
入口は等級です。形状の頂点1つ1つを画面上のどこへ置くかを計算する、GPU上の小さなプログラムです。頂点の数だけ実行されます。はpow(2.512, 6.5 - aMag)で強度、つまり等級から作る無次元の明るさを求めます。6.5等がちょうど強度1です。1等の差が約2.512倍という等級の定義そのもので、同じ式はAsterariumで、three.jsもReactも状態管理の仕組みも参照しない、天文計算などのモジュール群を指す呼び名です。ブラウザを立ち上げずに実行できるので、入力を与えて戻り値を確かめるだけのテストが書けます。にもあります(そちらは基準の等級を引数で選べます)。GLSLの文字列とTypeScriptの関数は共有できないので、この式だけは2か所にあります。基準の6.5等のほうは、この空と恒星間フライトのページが1つの定数を共有し、その一致をテストで固定しています。この6.5等は正規化の基準であって、描く下限ではありません。実際に描く下限は光害のない空で6.8等、暗い空での肉眼の限界の目安6等から6.5等より少し深いところです。消えかたは急ではなく、6.0等から6.8等のあいだで滑らかに薄れ、6.8等で完全に消えます。
ただし6.8等という下限は、全カタログを読み込む上位のAsterariumで、描画の重さをまとめて切り替える設定です。低いほうから低・中・高・最高(コード上はlow・medium・high・ultra)の4段があり、描く星は低が明るい順に6,000星まで、中が明るい側の8,037星、高と最高が全38,168星で、画面の解像度の上限と星のまたたきの有無も段ごとに変わります。実測のフレームレートに応じて自動で上下します。にいるときの話です。品質段階は実測の動く絵を作るときの1コマです。滑らかに見せるには毎秒60コマ前後を描き続ける必要があり、1コマぶんの持ち時間は16ミリ秒ほどしかありません。レートに応じて自動で上下し、下位の段階が使う明るい側のカタログは6.5等までしか含まないので下限はそこまで浅くなり、最下位はさらに明るい順に6,000星までしか描きません。
サイズは強度の0.25乗、つまり光量の4乗根で伸びます。このカタログでシリウスは−1.09等、強度にすると約1,100で、3等星の強度約25の40倍を超えます。それでも4乗根なので、描かれる直径の比は2.5倍ほどです。面積を光量に比例させれば直径は6倍を超え、明らかな塊になります。抑えた分は輝度へ回します。核の輝度は強度を1.0から12.0に収めた値で、中間バッファがHDRなので1.0超えが残り、最後のAgXが白へ寄せます。「眩しくて白く見える」を、色を白く塗らずに曲線の性質から起こしています。下限の1.0は暗い星のためのものではなく、6.5等より暗い星も核は1.0のまま、消えるのは輝度ではなくアルファです。
広がりかたを決めるのが点像分布関数、コードではstarPSFです。rはスプライトの中心を0、端を1とした半径で、第1項は鋭いガウシアンの核、第2項はべき乗で減衰するグレアの裾です。眼の中で散乱して視野を覆う光は、光源からの角距離のおよそ2乗に反比例します(Spencer et al. 1995やCIEのグレアモデル)。この2つは物理的な裏付けとして引いているのではなく、裾の形の着想の出所です。指数uSkirtPowは2.6で、裾を広げすぎないようモデルの2より急にしてあります。rも角距離ではなく、スプライトの中の位置です。式の中のr0の0.10は、中心でrが0になって裾の値が発散するのを防ぐためのもので、半径0.10より内側は0.10として扱い、裾をuSkirtGainで頭打ちにします。関数全体を滑らかに0へ落とす行が最後に要るのは、べき乗の裾がゆっくり減るだけで0にならないからです。端でも核の0.0004倍ほどが残り、そのまま切れば正方形のスプライトの縁で断ち切られます。半径0.75から1で0へ落とし、円形で終わらせています。
暗い星にはもうひとつ補正が要ります。先に挙げたサブピクセル補正です。サイズが1画素を下回ると、描かれるかどうかが画素の格子まかせになり、ちらつきます。そこで描く大きさを下限の1.5画素へ引き上げ、本来の直径と1.5画素の比の2乗をアルファに掛けます。直径の比の2乗は面積の比なので、星が出す光の総量は変わらず、暗い星は滑らかに薄れて消えます。
光害と瞬き
光害と瞬きも星ごとに乗ります。メニューの「表示設定」にある「光害」のつまみが選ぶのは0から8の9段階で、これは一般の光害の尺度とは対応させていない独自の目盛りです。限界等級6.8等は光害のない0の値で、段階が1つ上がるごとに0.55等ずつ下がり、8では2.4等になります。8を都心の空に近い目安として置いていますが、実測に基づく対応ではありません。薄れる帯もこれに合わせて動くので、8では2.4等で消え、1.6等から2.4等のあいだで薄れます。瞬きの振幅は高度で変え、地平線でほぼ全開、天頂ではほとんど止めます。大気を斜めに長く貫くほど揺らぎが強いからです。
2系統の排他描画と、調整用のつまみ
星は2系統で描かれます。一般の星は点の集合として、明るい星はビルボードとして描きます。分ける境界は1.7等で、カタログでここより明るい星はちょうど30個です。理由はハードウェアです。点の大きさにはGPUごとに上限があり、ブラウザの中からGPU(絵を描くための専用チップ)を直接使って3Dの絵を描く、Web標準の描画APIです。プラグインなしで、ページの中に立体的な場面を出せます。の仕様が保証するのは1画素だけ。実機の上限はもっと大きいものの、機種でばらつきます。しかも点は核も裾も含めた正方形をまるごと塗るので、大きいほど費用がかさみます。1辺28画素という頭打ちは、実機の上限を測って出したものではなく、塗る費用と見た目の折り合いで選んだ値です。保証される1画素より大きな値に頼ることになりますが、GPUの上限が28画素に届かない機種では、点の大きさがその機種の上限で切り詰められ、そこから大きくならないだけです。ただしシリウスやベガのグレアの裾は28画素より広がります。だから最も明るい星は点ではなくビルボードで描きます。上限の低い機種でも最も明るい星はビルボードの側を通るので、影響はそこまで明るくない星の見かけの大きさに限られます。ビルボードは四隅を持つ平らな面1枚のことで、三角形2つ分にあたります。絵を貼ったり、画面全体を覆ったりするのに使う、いちばん単純な形です。の大きさを自分で決められるので、同じ計算の値を6画素から30画素に収め、中心から縁までの長さに使います。幅にすれば最大約60画素、点の倍です。
明るい星のまわりの不規則なきらめきも、星ごとの乱数の種から5本から7本の筋を作ってビルボードにだけ足します。筋が模しているのは、明るい点を見たとき目の中で光が散って生じる放射状のにじみです。
問題は「同じ星を2回描かない」保証です。除外リストを別に持つのではなく、既存の描画則が自然に不可視化する値を入れました。明るい側かどうかを返す関数は1つ。ビルボードの側はその星を描き、点の側は同じ関数を呼んで等級を99というありえない値に置き換えます。
const magForPoints = data.mag.slice()
for (const i of brightStarIndices(data)) magForPoints[i] = 99
g.setAttribute('aMag', new BufferAttribute(magForPoints, 1))99等の強度はほぼ0です。サイズ則がサブピクセルの値を返し、エネルギーを保存する補正でアルファがほぼ0まで落ち、discardに到達します。判定する関数が1つしかないので、境界の1.7等を動かせば両側が同時に追従します。
ここまでに出た閾値と係数——アルファの0.003、裾の指数2.6、点の1辺28画素、2系統を分ける1.7等、筋の本数、光害のない空の限界等級6.8等と1段あたり0.55等——は、いずれも開発中に画面を見ながら決めた調整値です。URLのクエリ文字列で上書きできます。?exp=はAgXの曲線に入る前の露出倍率で、既定の1.0に対する倍率を書きます。/?exp=1.5なら1.5倍の明るさです。?starlaw=は星の描画則の6つの既定値(サイズのゲイン、サイズの指数、核の上限、裾のゲイン、裾の指数、きらめきの量)をこの順のカンマ区切りの数値でまとめて上書きします。