空、夕焼け、惑星のレンダリング
(blog.maximeheckel.com)- ブラウザシェーダーが Rayleigh散乱、Mie散乱、オゾン吸収を組み合わせて、青空と夕焼け・朝焼けをリアルタイムにレンダリングする
- カメラ光線の 光学的深さ と Beer's Law の透過率を積算し、位相関数で太陽方向に応じた散乱分布を計算する
- 夕焼け効果は各サンプルで太陽方向に別途 light-march を実行し、太陽光が大気を通過しながら失う量を反映する
- 平面スカイシェーダーは depth buffer とワールド座標の復元によって ポストプロセス効果 となり、シーンオブジェクト間の大気フォグまで処理する
- 惑星スケールでは logarithmic depth buffer、ray-sphere intersection、LUT ベースの Transmittance・Sky-view・Aerial Perspective へと拡張される
大気散乱シェーダーの目標と参考資料
- スペースシャトル Endeavour の低軌道での夕焼け写真のように、地球上層大気の 暗いオレンジ色・青色・宇宙背景の黒 へと続く層をブラウザシェーダーで再現することが目標である
- 実装範囲は、raymarching、Rayleigh散乱、Mie散乱、オゾン吸収を組み合わせたリアルな sky dome から始まり、惑星周辺の大気シェルと LUT ベースの最適化へと拡張される
- 主な参考資料は Three Geospatial、Sébastien Hillaire の A Scalable and Production Ready Sky and Atmosphere Rendering Technique、Atmospheric Scattering (and also just faking it) である
空のレンダリングの基本モデル
-
単純なグラデーションでは不十分な理由
- 空の色は単なる青い背景ではなく、光が空気とその構成要素と相互作用した結果として扱う必要がある
- 観測者の高度、塵の量、時間帯といった変数を考慮する必要があり、計算は ボリューム(volume) の中で行われる
-
大気密度のサンプリング
- 大気は volumetric clouds や volumetric light のように raymarching でサンプリングする
- カメラ位置から光線を飛ばし、透明な媒質に沿って前進しながら、大気を通過して生き残る光である 透過率(transmittance) と、各サンプルでカメラ方向へ再指向される 散乱(scattering) を計算する
- raymarching の復習資料として Painting with Math: A Gentle Study of Raymarching を参照できる
-
Rayleigh密度と光学的深さ
- 透過率を求めるには、光線が通過しながら出会う大気密度を積算して 光学的深さ(optical depth) を計算する必要がある
- Rayleigh密度関数は高度
hにおける「空気」の量を表し、高度が上がるほど大気が希薄になる効果を反映する - 実装例では
RAYLEIGH_SCALE_HEIGHT = 8.0km、ATMOSPHERE_HEIGHT = 100.0km、VIEW_DISTANCE = 200.0km、PRIMARY_STEPS = 24を使用する rayleighDensity(h)はexp(-max(h, 0.0) / RAYLEIGH_SCALE_HEIGHT)で、ループ内ではviewOpticalDepth += dR * stepSizeとして積算される
-
Beer's Law と昼の空の青さ
- 光学的深さから特定地点の透過率
Tを計算し、T=1.0は光損失なし、T=0.0は光が完全に消えたことを意味する - 透過率は Beer's Law で計算され、実装例では
vec3 transmittance = exp(-rayleighBeta * viewOpticalDepth)を使用する rayleighBetaは Rayleigh散乱係数であり、シェーダーではvec3(0.0058, 0.0135, 0.0331)として保存される- 太陽光方向と視線光線の間の角度は
3.0 / (16.0 * PI) * (1.0 + mu * mu)という形の Rayleigh phase function でモデル化される - Rayleigh散乱係数のため、赤はほとんど散乱されず、緑はやや多く散乱され、青が最も多く散乱されるため、昼の空は青く見える
- これをピクセルごとに1本の光線へ拡張すると、地平線方向ではより多くの大気を通過するため明るい白いもやのように見え、高度が上がるほどより深く暗い青へと変化する
- 光学的深さから特定地点の透過率
Mie散乱とオゾン吸収
-
Rayleighだけでは足りない効果
- Rayleigh散乱だけでも十分に見栄えのする結果は得られるが、より現実的な空には追加の大気効果が必要である
- Mie散乱 は塵やエアロゾルのようなより大きな粒子と光の相互作用を表し、密度関数と方向ごとの再分配を表す位相関数を持つ
- オゾン吸収 は上層大気を通過する光の一部波長を散乱させるのではなく経路から取り除く
- オゾン吸収は特に地平線、夕焼け、朝焼け前後の薄明で空の色をより深くし、色相を移動させる
-
Mieとオゾンの積算
- Rayleigh、Mie、オゾンを併用する実装では、それぞれの光学的深さを
viewODR、viewODM、viewODOに積算する - 各サンプルでは
dR = rayleighDensity(h)、dM = mieDensity(h)、dO = ozoneDensity(h)を計算し、tauはBETA_R * viewODR、BETA_M_EXT * viewODM、BETA_OZONE_ABS * viewODOの和で構成される - 透過率は
exp(-tau)で計算され、sumR、sumM、sumOには各密度と透過率、stepSizeが積算される - 最終的な散乱は
SUN_INTENSITY * (phaseR * BETA_R * sumR + phaseM * BETA_M_SCATTER * sumM + BETA_OZONE_SCATTER * sumO)の形で計算される
- Rayleigh、Mie、オゾンを併用する実装では、それぞれの光学的深さを
-
主な定数と効果
MIE_SCALE_HEIGHTはエアロゾル用のRAYLEIGH_SCALE_HEIGHTに相当し、粒子は通常地平線近くに集中するため、より小さい1.2kmに設定されるMIE_BETA_SCATTERは粒子がどれだけ光をカメラ方向へ散乱するかを制御し、ほとんどが波長非依存であるためvec3(0.003)に設定されるMIE_BETA_EXTは経路からどれだけの光が除去されるかを示す Mie 消滅係数であり、遠方の大気をより霞んで見せるMIE_Gは異方性を制御し、0.0は一様散乱、1.0はより強い前方散乱バイアスを意味するOZONE_BETA_ABSはvec3(0.00065, 0.00188, 0.00008)の値を持ち、緑や黄橙系をより多く吸収して、空の色を青・赤・紫方向へ移動させる- Mieとオゾンを統合すると、より自然な「sky blue」の色と太陽周辺の霞んだ光のにじみが生まれ、太陽が地平線近くにあるときには Mie散乱の効果がよりはっきり現れる
光の経路と夕焼け・日の出
-
既存実装の限界
- sky fragment shader はさまざまな高度で自然な色をレンダリングでき、Mie、Rayleigh、オゾンの透過率モデルを反映できる
- しかし太陽を地平線近くへ移動させても、光の減衰や夕焼け・日の出の効果はなく、白くぼんやりした光の塊が現れるだけだった
- 既存の raymarching ループが、カメラから各サンプルまでの視線レイ上でしか光の減衰を計算していなかったためである
- サンプル地点に到達する前に、太陽光が大気を通過する間にどれだけ失われるかも計算する必要がある
-
light-march のネストされたループ
- 各サンプル地点で光源方向に別のネストされたループを回し、その経路の透過率をサンプリングする
- 関連するアプローチは real-time cloudscapes と volumetric lighting でも使われている
lightMarch(float start, float sunY)はLIGHTMARCH_STEPS回繰り返しながらodR、odM、odOを蓄積する- 既存実装の光学的深さ
viewODR、viewODM、viewODOに、太陽方向の光学的深さsunODを加える - 最終的な
tauはBETA_R * (viewODR + sunOD.x)、BETA_M_EXT * (viewODM + sunOD.y)、BETA_OZONE_ABS * (viewODO + sunOD.z)を足し合わせて構成される - この実装により、夕焼け、日の出、天頂の太陽、その中間の照明条件にある空をレンダリングできる
sun angleuniform により、一日の中での空の青の変化を作り出し、Mie 散乱は夕焼けや日の出で光を地平線と自然に混ぜ合わせる- 太陽が低いとき、オゾンは空に紫がかったトーンを加える
惑星大気への拡張
-
平面背景からポストプロセス効果へ
- 先に作成したシェーダーは優れた空の背景を提供するが、React Three Fiber シーンの平面背景に近い
- 次の段階は、これを**ポストプロセス効果(post-processing effect)**に変えて、シーンの深度を考慮するボリュームとして、また惑星メッシュを取り囲む大気のシェルとしてレンダリングすることだ
- そのために
screenUV座標からワールド空間座標を再構成し、シーンの depth buffer を raymarching に反映する
-
ワールド空間の再構成と 3D レイ
- 大気散乱をシーンに適用するには、空だけを描くのではなく、カメラと画面にレンダリングされたオブジェクトの間の空間を満たす必要がある
- 必要なデータはシーンの depth buffer、カメラの
projectionMatrixInverse、matrixWorld、positionであり、これらの値をポストプロセス効果の uniform として渡す getWorldPosition(vec2 uv, float depth)はdepth * 2.0 - 1.0でclipZを作り、uv * 2.0 - 1.0で NDC 座標を作ったあと、projectionMatrixInverseとviewMatrixInverseを適用する- 同じ手順は On Shaping Light の volumetric lighting ポストプロセス効果でも使われている
- 現在のピクセルの
worldPositionを取得したあと、rayOriginはカメラ位置、rayDirはnormalize(worldPosition - rayOrigin)として計算し、画面上のピクセルごとの3D レイに沿って進む
-
深度バッファで raymarch 区間を調整
- シーンジオメトリを考慮するには、固定の
stepSizeではなく、depth buffer によって現在のレイの raymarch 区間を決める必要がある sceneDepth = depthToRayDistance(uv, depth)により、レイ上のシーン深度を求める- 背景ピクセルは
depth >= 1.0 - 1e-7で判定し、“sky pixels” にはsceneDepth = atmosphereHeight * SKY_MARCH_DISTANCE_MULTIPLIERを適用する - レイが下向きなら
tGround = observerAltitude / max(-rayDir.y, 1e-4)で地面との交差を計算し、rayEnd = min(rayEnd, tGround)で制限する - 最終的な
stepSizeは(rayEnd - rayStart) / float(PRIMARY_STEPS)で計算する - 近いオブジェクトや地面に当たるレイは小さな
stepSizeでより正確にサンプリングされ、遠くまで進むレイは同じ数のサンプルをより長い距離に分布させる
- シーンジオメトリを考慮するには、固定の
-
シーン内の大気フォグ
- ポストプロセス効果として実装されたシェーダーは、シーン全体のボリュームに大気散乱を適用し、シーンジオメトリを考慮しながら sky shader を背景として使える
- カメラに近いオブジェクトはより鮮明に見え、遠くのオブジェクトはより強くぼやける
Raycasterでドラッグ可能な天体を入れたインタラクティブな例は、MaximeHeckel のツイート で確認できる
惑星のレンダリング
-
必要な2つのステップ
- 惑星の周囲に現実的な大気をレンダリングするには、大きなスケールを扱うための logarithmic depth buffer と、光線が大気中のどこで始まりどこで終わるかを定義する球状の大気シェルが必要
-
logarithmic depth buffer
- 惑星スケールでは遠方から見たときに、大気と惑星シェルの深度差をシェーダーが区別しづらくなり、depth fighting が発生することがある
- 大気の高さは数 km にすぎないため、シーンの depth buffer の定義と、後処理エフェクトでの読み取り方法の両方を調整する必要がある
- React Three Fiber の
Canvasを包むglprop でlogarithmicDepthBuffer: trueを設定する - 設定例は
<Canvas shadows gl={{ alpha: true, logarithmicDepthBuffer: true }}>の形 - シェーダーでは logarithmic depth buffer を光線上の距離へ戻すため、
sceneDepthの計算を再定義する logDepthToViewZ(depth)はpow(2.0, depth * log2(cameraFar + 1.0)) - 1.0を使い、-dを返す
-
ray-sphere intersection で大気区間を見つける
- 視線の光線が 大気球(atmospheric sphere) に入る地点と出る地点を見つけるために、ray-sphere intersection test を使う
- 2つの交点を得られれば、大気の外でサンプルを無駄にせず、その区間だけに raymarching ループを制限できる
- 惑星は球形メッシュで、それより少し大きな大気球が取り囲む形なので、同じ交差テストを惑星自体にも行う
- 光線が大気を抜ける前に地表に当たる場合は、地表との交点を raymarching 区間の終点として使う
- 使用した
raySphereIntersectの実装は Inigo Quilez の Ray-Surface intersection functions を参考にしている
-
シーンオブジェクトと大気の終了条件
- 大気は惑星表面に達した時点、または地表に達する前に別のシーンオブジェクトに当たった時点で終了しなければならない
- 惑星に当たる場合は基本的に
atmosphereFar = min(atmosphereFar, planetHit.x)として地表で止める - 他のメッシュが地表より手前にレンダリングされている場合は、
sceneDepth < planetHit.x - 2.0条件で判定し、atmosphereFar = min(atmosphereFar, sceneDepth)を適用する - このロジックがないと、惑星表面がオブジェクトより前に現れてしまう問題が生じる
-
React Three Fiber デモと残っているグリッチ
- 2つの調整をコードに反映すると、大気散乱を後処理エフェクトとして実装し、惑星周囲の大気をレンダリングできる
- デモシーンでは React Three Fiber でシンプルな “Sun - Earth system” をレンダリングし、カスタムエフェクトを適用している
- 太陽の位置を調整してズームアウトすると、地上から軌道までさまざまな角度でシェーダーが作る空の色を見られる
- 同じ効果は4月初旬の記事予告用ポスター画像にも使われており、レンダリング画像は ツイート で共有されている
- シーン内の torus は日没後でも依然として “lit-up” 状態に見えることがある
- 原因は主 directional light の shadow-map または shadow-camera のスケールが小さく、遠すぎる torus をカバーできないため
- 回避策として volumetric lighting article の shadow-mapping アプローチを再利用できるが、実際には試していない
日食の処理
- 大きな天体が太陽を遮る場合 は
lightMarchの後でsunVisibility関数を呼び出し、その戻り値[0, 1]を透過率に掛ける形で追加できる - 基本的なアイデアは、現在のサンプル地点から見た 月の方向 と 太陽の方向 の内積を比較すること
- 2つの方向がほぼ同じで内積が
1.0に近ければ月が太陽を遮っている状態で、直交して0.0に近ければ遮りはない - 単純な内積だけではシーン内オブジェクトの サイズとスケール を反映できないため、実装では太陽と月の角距離、およびそれぞれの角半径を比較する
sunVisibilityは、月が太陽を遮らない場合、カメラ視点から月が太陽より大きいか同程度の大きさに見える状態で遮る場合、カメラ視点から月が太陽半径の内側に入る状態で遮る場合を扱う- デモでは既存の大気散乱サンプルに
sunVisibilityと 月メッシュ を追加し、月を太陽と整列させたときの光不足の状況を Atmospheric Scattering シェーダーが処理するようにしている - より精密な日食とコロナのシミュレーションは Physically Based Real-Time Rendering of Eclipses の論文で扱われているが、その実装は WebGL へ移植していない
他の惑星の大気
- 使用した大気密度と散乱モデルは、惑星と大気の半径、
RayleighScaleHeight,RayleighBeta,MieScaleHeight,MieBeta,mieBetaExt,mieG,OzoneHeight,OzoneWidthといったいくつかの定数によってほぼ決まる - これらの値を調整すれば、火星の大気 や他の惑星の大気に近い結果を作れる
- 火星向けに使った値は近似値
planetRadius: 3390atmosphereRadius: 3500, 約110 kmの厚さrayleighScaleHeight: 11.1rayleighBeta: new THREE.Vector3(0.019, 0.013, 0.0057)mieScaleHeight: 1.5mieBeta: 0.04mieBetaExt: 0.044mieG: 0.65ozoneCenterHeight: 0.0ozoneWidth: 1.0ozoneBetaAbs: new THREE.Vector3(0.0, 0.0, 0.0)sunIntensity: 15.0planetSurfaceColor: '#8B4513'
- 既存の定数をこれらの値に置き換えると、より ほこりっぽくオレンジがかった大気 になり、火星特有の 夕焼け時の青み も得られる
- 関連論文として Physically Based Rendering of the Martian Atmosphere がある
LUT ベースの大気散乱
-
アプローチと簡略化した部分
- 従来のシェーダーは小さなスケールと大きなスケールの大気を直感的にレンダリングできる一方、
PRIMARY_STEPSの多いレイマーチングループ、入れ子になったlightmarchingループ、フルスクリーン解像度での計算のため実行コストが高い - Sebastian Hillaire の A Scalable and Production Ready Sky and Atmosphere Rendering Technique は、コストの高い散乱計算をテクスチャに保存し、最終レンダリングでは事前計算済みのテクスチャをサンプリング・合成する Look Up Tables(LUTs) ベースの方式を提案している
- 扱う LUT は、光が大気を通過する間にどれだけ生き残るかを保存する Transmittance LUT、特定のカメラ位置での空の色を保存する Sky-view LUT、カメラと見えているシーンジオメトリの間の大気ヘイズと散乱光を保存する Aerial Perspective LUT である
- 論文全体の実装をそのまま移植したわけではなく、LUT は WebGPU の compute shader に向いているが、時間不足と記事の連続性のため WebGL を維持している
- 論文では Aerial Perspective LUT は 3D texture だが、実装では 2D render target を使用している
- この方式ではカメラが動くたびに正しいピクセル値のためにテクスチャを再生成する必要があり、事前計算しておくのが難しい
- Multi-Scattering は時間不足のため省略した
- 従来のシェーダーは小さなスケールと大きなスケールの大気を直感的にレンダリングできる一方、
-
Transmittance LUT
- 従来のシェーダーでは、すべてのサンプル地点が
lightmarchを呼び出して太陽光がどれだけ到達するかを計算しており、この処理は高コストだった - Transmittance LUT はこのデータを低解像度で事前保存し、その後ほかの LUT が光データを必要とするときに読み出して使えるようにする
- 実装では
250 x 64解像度の専用 Frame Buffer Object を定義し、カスタムシェーダー material を専用シーンtransmittanceLUTSceneの full-screen quad に適用したうえで、レンダリング結果のテクスチャを downstream LUT の uniform として渡す - 各ピクセルでは
vec3(0.0, radius, 0.0)からレイマーチングを行い、radiusはvUv.y座標に沿ってplanetRadiusからatmosphereRadiusまで増加する - LUT の x 軸 は光の角度、y 軸 は高度を表し、純白は
100%透過率、黒または色の付いた領域は地面または空気が最も厚い部分を示す - 以降の LUT は「与えられた角度と高度で大気を通過して生き残る光の量」をテクスチャ参照だけで取得できる
- 従来のシェーダーでは、すべてのサンプル地点が
-
Sky-view LUT
- Sky-view LUT は、特定の方向を地上から見上げたとき空がどのような色になるかを計算する
getSkyViewRayDirはvUv.xを azimuth[-PI, PI]に、vUv.yを elevation[-PI/2, PI/2]にマッピングしてレイマーチング方向を定義する- elevation には
(vUv.y * vUv.y - 0.5) * PIという quadratic mapping を使っており、遠距離で Sky View が過度にちらつくのを避けるための回避策である - レイが大気に入らなければ黒を返し、惑星に当たるレイは見えている大気区間だけをレイマーチングし、惑星に到達したらさらに早く停止する
- 散乱ループは以前と同じだが、Sky View 方向に沿って進み、太陽光には Transmittance LUT を使う
-
Aerial Perspective LUT
- Hillaire 論文とは異なり、実装結果は 2D テクスチャ であり、各ピクセルは見えている画面ピクセル 1 つに対応する
- シーンの depth buffer を使って、そのレイに沿ってどこまで進み、どれだけ散乱を蓄積するかを決定する
- 従来の散乱コードをほぼ再利用しつつ、各サンプルが Transmittance LUT から太陽光の可視性を取得する
- 出力は RGB に蓄積した大気散乱を保存し、アルファには合成時に使う packed view transmittance 値を保存する
- 実装の流れは、
depthBufferから深度を読み、getWorldPosition(vUv, depth)で画面ピクセルのワールド空間位置を復元した後、カメラ位置からワールド位置までのrayDirを計算するというものだ - 続いて
logDepthToRayDistance(vUv, depth)でシーン深度をレイ距離に変換し、大気と惑星の交差を計算したうえで、見えている大気区間だけを march する
-
合成
- Sky-view LUT と Aerial Perspective LUT を生成したあと、最後の post-processing pass で両者を結合する
- 中核となる作業は、現在の
rayDirを Sky View UV 座標 に変換することだ - シーンジオメトリには Aerial Perspective LUT を適用し、アルファチャンネルを view transmittance として、RGB チャンネルを散乱光として使い、
color = color * aerialPerspective.a + aerialPerspective.rgbを計算する - 背景ピクセルには Sky View LUT をサンプリングし、
depth >= 1.0 - 1e-7であれば背景と見なしてcolor = inputColor.rgb + sampleSkyViewLUT(rayDir, planetCenter)を適用する - 最後に
ACESFilm(color)とpow(color, vec3(1.0 / 2.2))を適用する - LUT ベースの大気実装コード全体は Github link で確認できる
まとめ
- LUT ベースの大気散乱の結果は以前の完全なレイマーチング版とほとんど同じに見えるかもしれないが、内部処理は異なる
- 作業をより小さな LUT 群に分割し、最後の効果で合成することで、各サンプルごとに太陽方向へ繰り返しレイマーチングして到達光を計算しない
- Transmittance LUT から照明情報を直接取得するため、高コストな入れ子ループを単純なテクスチャ参照に置き換え、最終シーンで無視できない性能向上を得られる
- 実装は Sébastian Hillaire や他分野の実装と比べると不十分で、とくに Sky View には banding や flickering があり、簡略化した部分のため最適性も低い
- 最初から WebGPU を使うべきだった可能性がある
- 実際の production-grade 実装として、Shoda Matsuda(@shotamatsuda) の three-geospatial を勧める
- さらに volumetric clouds を重ねる作業も行ったが、結果はまだ一長一短で、記事で見せられるほど満足のいくものではなく、さらに作業が必要である
1件のコメント
Hacker Newsのコメント
視覚効果を開発し、それがだんだん現実らしく実装されていくのを見るのには特別な面白さがあって、いつか自分でもこの分野を実験してみたい
以前は動画の再生数が数百万だったのに、今では50万をようやく超える程度。コロナ禍で皆が家にいて、ランダムなものに関心を持っていた影響かもしれない
ふだん寝るときに流しているけれど、落ち着いていながら深く技術的なテーマを掘り下げるこういうコンテンツがもっとあればと思って、自分で作ってみようかと考えたこともある
日没後もしばらくの間、頭上の大気や地平線上の領域にはまだ日光が当たっており、地球の大気では太陽が地平線下 18度 まで下がるあいだは目立つ薄明が残る。レイトレーシングで実装するのは実用的でないかもしれないが、これをモデル化する一般的なアルゴリズムはある
https://www.threads.com/@mrsharpoblunto/post/DVS4wfYiG8f?xmt...
https://www.threads.com/@mrsharpoblunto/post/C6Vc-S1O9mX?xmt...
https://www.threads.com/@mrsharpoblunto/post/C6apksDRa8q?xmt...
1993年の論文であり、この分野の元祖に近く、とても読みやすい Nishita らの “Display of The Earth Taking into Account Atmospheric Scattering” を実装したことを思い出す: https://www.researchgate.net/publication/2933032_Display_of_...
動くようにできたとき、「この複雑な現実世界の現象を、比較的単純な計算をいくつかするだけでかなりうまくモデル化できるんだな」という瞬間があった。静的な青空のスカイボックスから、一気に完全な昼夜サイクルへ進んだ
以前、Web で空を複数の グラデーション を重ねてレンダリングしたらどうだろうと考えたことがある。ある程度はうまくいって、そこそこの結果は得られたかもしれないが、ここで作られているものとは比べものにならない。成果物は印象的で刺激を受ける
それだけでもかなりもっともらしい夕焼け/日の出のサイクルが出せて驚いたし、記憶が正しければ太陽そのものもそこから自然に現れてきた。Microsoft の C# ゲーム開発プラットフォーム XNA を使いながら、Riemer の優れたチュートリアルシリーズを追っていて、アーカイブはここにある https://github.com/SimonDarksideJ/XNAGameStudio/wiki/Riemers...
ただ、散乱に関する内容は見当たらないので、その部分は別のところから持ってきたのかもしれない。数式入りの論文を読んでいた記憶はある
https://spaceengine.org/
「SpaceEngine にはオブジェクトがいくつありますか?」への答えは、Hipparcos 星表の全体、既知の系外惑星のすべて、1万を超える銀河、太陽系の大半の天体を合わせて13万個で、さらに観測可能な宇宙全体に実際に存在する数よりも多い銀河や恒星系が追加されるというもの。「水の惑星はどうして熱くなり得るのですか?」には、上層大気の水は高温の水蒸気だが、下へ行くと高圧のもとで液体へと滑らかに遷移し、さらに深部では ice VII という固体相になると答えている。「どうやって移動するのですか?」の答えは WASD キー
素晴らしいゲームだし、かなり古いのに、これほど良いものはまだ見たことがない
この記事を見て自分も SpaceEngine を思い出した
自分の好きな論文の一つ: http://www.graphics.stanford.edu/papers/bssrdf/bssrdf.pdf
牛乳をレンダリングするのが厄介な問題だということを、たぶんこのとき初めて知った
たぶん5%くらいしか理解できなかったけれど、本当に強く感心した
しかも MITライセンス なら、自分のゲームのスカイボックス問題は解決したも同然。遠近は固定されるはずだから、太陽が空を横切るレンダリングさえあればよく、そこにサイン波周期で年間の太陽角度変化を拡張できる