- 空の背景用の月とガス惑星は、実際の球メッシュがなくても、円形ディスクとピクセルシェーダだけで表面、回転、大気表現を処理できる
- 従来の UV sphere は 2×1 テクスチャのマッピングは簡単だが、角ばった輪郭、mipmap サンプリングの損失、反復パターン、seam、極点の歪み、大気 halo の限界が同時に生じる
- ディスク方式では、中心 UV から球表面位置を復元した後、
asin ベースの球面 UV、回転行列、u 座標の移動による spin で、球のように見える表面をピクセル単位で作る
- シェーディングは、表面位置を normal として使う Lambertian ライティングから始め、岩石表面には TBN 行列と normal map を適用し、大気は halo の明るさとチャネルごとの吸収を近似する
- Substance Designer の静的テクスチャと Unreal の動的レンダリングテクスチャを同じ方法で扱えるため、より小さな動的テクスチャと tiling 調整で性能負荷を減らせる
スカイボックス惑星の視覚目標
- プロジェクトのスカイボックスは、アニメーションする月とガス惑星で異星の空のような構図を作ることに焦点を当てている
- 2 つの惑星は回転し、ガス惑星には動く大気の流れも入る
- 実際の動きは微妙だが、視覚効果のために速度を上げている
- アートディレクションは、1970〜80 年代の SF アートに着想を得た**セミリアリスティック(semi-realistic)**なスタイルである
表面テクスチャ入力と制約
- ガス惑星の表面は、ピクセルシェーダと render-to-texture 方式でリアルタイム生成する
- 月のテクスチャは Substance Designer で作成し、カラーパレットと material seed を変えてさまざまな look を試せる
- 動的なガス惑星は毎フレームテクスチャをレンダリングする必要があり、性能コストが大きい
- テクスチャ解像度を 2 倍にすると、レンダリングコストは 4 倍になる
- 静的な Substance Designer テクスチャと、Unreal で動的にレンダリングしたテクスチャの両方で一貫して動作する解決策が必要である
- cubemap も当初は検討されたが、正方形テクスチャに比べて6 倍のリソースを使う可能性があり、Substance Designer が cubemap 生成をサポートしていないため実用的ではない
UV sphere で明らかになった限界
- UV sphere は 2×1 の長方形テクスチャと座標を合わせやすいため、基本の選択肢に見える
- 一度に球の片面しか見えないので、全体固有の長方形テクスチャではなくタイル可能な正方形テクスチャを使うこともできる
- 実際に適用すると複数の問題が重なる
- 輪郭が目に見えて角張って見え、subdivision を増やすかピクセルシェーダのマスクで隠す必要がある
- 球表面の大半は斜めに見えるため低い mipmap からサンプリングされ、動的テクスチャでは毎フレーム生成したピクセルのかなりの部分がほとんど見えない領域に使われる
- tiling は整数倍で増やさないと全体パターンがつながらず、2 倍にしただけでも反復が目立つ
- fractional tiling は北極から南極まで続くseamを作る
- 極点では texture pinching と三角形マッピングの歪みが生じ、PSX の揺れるテクスチャのような歪みに似て見える
- 表面しか描けないため atmospheric halo には別モデルが必要になる
- UV sphere は惑星モデリングには依然として使えるが、スカイボックス用途では修正や hack が多くなる
円形ディスクとピクセルシェーダによるアプローチ
- スカイボックスの惑星は遠方から単一の位置で観察されるため、複雑な 3D 球メッシュは必須ではない
- シンプルな塗りつぶし円形ポリゴンディスクを使い、ピクセルシェーダでテクスチャマッピングを処理する
- 同じメッシュ内で atmospheric halo まで描画できる
- ディスクの UV 原点は正確に中心にある必要がある
- 惑星半径を 1 とすると、大気を描く空間を確保するため UV 座標はその外側まで拡張されていなければならない
球表面の復元
- 座標系には DirectX と一貫したleft-handed, Y-upシステムを使う
- Unreal Engine も left-handed だが up 方向は Z なので、向きと normal map のフォーマットを確認する必要がある
- 表面方程式は、ディスク平面上の 2D 位置を球表面上の 3D 位置にマッピングする
- まずピクセルが球表面内にあるかを判断するため、UV ベクトル長が半径より小さいかを確認する
float CircleMask( float2 uv, float radius)
{
return length(uv) < radius? 1.0: 0.0;
}
- この方式は、UV 座標がメッシュ中心に揃っている場合にのみ正しく動作する
- 球表面の local position は UV の x, y をそのまま使い、z だけを復元する
float3 ReconstructSurface(float2 uv)
{
float zSquared = 1.0 - dot(uv, uv);
float z = sqrt(zSquared);
return float3(uv, z);
}
球面 UV の生成
- 正方形テクスチャを球表面にマッピングする過程は 3 段階に分かれる
- 円筒に巻き付ける
- y 軸にも同じ処理を繰り返す
- 結果を円形に変形する
- x テクスチャ座標は円筒を巻く角度に比例し、
arcsine(x)を計算した後、[-π, π]を[0, 1]に再マッピングする
- 球の local width であるgeneratrixを使って極点で座標を pinching する
float2 GenerateSphericalUV(float3 position)
{
float width = sqrt(1.0 - position.y * position.y);
float generatrixX = position.x / width * sign(position.z);
float2 generatrix = float2(generatrixX, position.y);
float2 uv = asin(generatrix) / 3.14159 + float2(0.5, 0.5);
return float2(uv);
}
軸の傾きと回転
- より自然な惑星構図のため、pitch と roll で惑星の軸を傾ける
- SF アートの惑星は通常傾いており、構図が動的に見え、極地の ice cap を見せることができる
- yaw は回転運動に相当し、seam 問題を避けるため別ステップで処理する
- メッシュ球では通常、惑星を行列で変換するが、ディスク方式では各ピクセルが表面位置を定義するため、回転だけを含む単純な 3×3 行列を使える
float3x3 CreateRotationMatrix(float pitch, float roll) {
float cosPitch = cos(pitch);
float sinPitch = sin(pitch);
float cosRoll = cos(roll);
float sinRoll = sin(roll);
return (float3x3)(
cosRoll, -sinRoll * cosPitch, sinRoll * sinPitch,
sinRoll, cosRoll * cosPitch, -cosRoll * sinPitch,
0.0, sinPitch, cosPitch
);
}
- Unreal の material editor は行列をデータ型としてサポートしていないが、回避方法はある
scale、seam、spin の処理
- テクスチャ scale を変えると、元のテクスチャ端に合わせて隠れていたUV seamが露出する
- seam を完全に簡単に消すことはできないが、背面に移せば目立ちにくくなる
- 球を position sign を基準に象限へ分け、back-left と back-right の象限に u 方向 offset を適用する
float2 GenerateSphericalUV(float3 position, float spin, float scale)
{
float leftRightSign = sign(position.x);
float frontBackSign = sign(position.z);
float width = sqrt(1.0 - position.y * position.y);
float generatrixX = position.x / width * frontBackSign;
float2 generatrix = float2 (generatrixX, position.y);
float2 uv = asin(generatrix) / 3.14159 + float2 (spin, 0.5);
if(frontBackSign < 0.0)
{
uv = float2 (uv.x + 1.0 * leftRightSign, uv.y);
}
return float2 (uv / scale);
}
- seam は依然として北極と南極を結んでいるが、背面へ移ったことでかなり目立たなくなる
- 回転は球自体を回すのではなく、u 座標方向に表面テクスチャを移動させて処理する
float2 GenerateSphericalUV(float3 position, float scale, float spin)
{
float width = sqrt(1.0 - position.y * position.y);
float generatrixX = position.x / width * sign(position.z);
float2 generatrix = float2(generatrixX, position.y);
float2 uv = asin(generatrix) / 3.14159 * scale + float2(0.5 + spin, 0.5);
return float2(uv);
}
float2 sphericalVU = GenerateSphericalUV(position, scale, time*speed)
表面シェーディングと normal map
- 惑星を丸く見せるにはシェーディングが重要である
- 一般的なエンジンのシェーディングと異なり、惑星は表面と大気という 2 層を持つため、両レイヤーのライティングと blending を手動で定義する必要がある
- 半径が 1 の場合、回転前の表面位置はそのまま surface normal として使える
- ライティングには単純な Lambertian モデルを使用する
- 目標は完全な写実性ではなく、NASA 写真に着想を得たスタイライズドな SF 表紙美学である
float LambertianLight(float3 normal, float3 lightDirection) {
float NdotL = max(dot(normal, lightDirection), 0.0);
return NdotL;
}
- gas giant のような滑らかな表面なら単純な normal で十分な場合がある
- 岩石型の惑星は、山脈、尾根、クレーターのような地形を normal map でシミュレートするほうが適している
- normal map を使うには、Tangent、Bitangent、Normal から成るTBN 行列が必要になる
- 球ではこの計算をピクセル単位で行う必要がある
- TBN 行列は normal map の normal を World Space Coordinates に変換するために使われる
UV の不連続性と mipmap アーティファクト
- ピクセルシェーダで生成した UV には**不連続(discontinuity)**の問題が生じることがある
- 背面 seam では水平 UV 成分が境界で 0.0 と 1.0 として完全につながる必要があるが、実際には seam に沿って blocky artifact が発生する
- この線は UV の DDX に対応している
- DDX と DDY は画面空間軸における UV の変化率を測定する
- テクスチャサンプラはこの値を使って使用する mipmap を決める
- 低い UV derivative は高解像度 mipmap に、高い derivative は低解像度 mipmap に対応する
- seam の左右で値が急激に跳ぶと DDX が大きくなり、最も低い mipmap からサンプリングされて灰色の線のように見えることがある
- 惑星では polar patch で隠される予定なので大きな問題ではないが、目立つ場合は seam の DDX と DDY を手動補正して sampler に渡す必要がある
- 例の fix は説明用であり、実際の値はマッピングに合わせて数式を自分で導出する必要がある
極地パッチと pinching 補正
- 極点の歪みは、実証済みの方法どおりpolar patchで覆う
- 惑星や月では極地が氷に覆われ、他地域と視覚的に区別されることが多いため、別テクスチャのパッチは問題を隠しつつ視覚的な面白さも加える
- polar patch は、回転軸に垂直な平面へマッピングし、その結果座標を回転させる方法で追加できる
float2 PolarPatchMapping(float3 position, float scale, float spin)
{
float cosSpin = cos(spin);
float sinSpin = sin(spin);
float scale = 0.4;
float2 uv = float2(position.x, position.z) * scale;
float2 spinningUV = float2(uv.x * cosSpin - uv.y * sinSpin,
uv.x * sinSpin + uv.y * cosSpin);
return spinningUV;
}
- UV map を頂点に保存せずピクセルごとに生成するため、極点 pinching 補正の柔軟性が高い
- 単純な除算で極点テクスチャの aspect ratio を補正できる
uv = float2(uv.x, uv.y/pow(width, 1/3));
- 補正結果は別の artifact のように見えることもあるが、現実的なシーンでは polar patch とより滑らかにつながり、視覚的不一致が減る
大気 halo の近似
- ディスクでは惑星表面が全領域を占めないため、余った空間にatmospheric haloを描ける
- 同じシェーダ内で halo を描けば、惑星上の大気と自然に合成できる
- 物理ベースの大気レンダリングは通常 Rayleigh scattering と raymarching を使うが、ここでは用いない
- Quake 1・2 の動的オブジェクト照明 trick に近い近似を使う
- Quake の static lighting は lightmap に事前計算されていた
- 動的なモンスターは足元の lightmap をサンプリングして色を調整し、物理的に正確ではなくても周囲になじませていた
- 同じ要領で、惑星表面を使って大気 halo のライティングを近似する
- bump mapping 前の表面 normal を拡張して halo の明るさを計算する
- チャネルごとに明るさを異なるように remap すれば、異なる光の波長吸収を簡易的にシミュレートできる
- 大気は斜め角度でより見えやすいため、surface normal の Z 成分を remap する
- halo の fade 効果のため、球表面からの距離も計算する
- 最後に表面と大気を alpha blending で合成する
最終結果と活用
- 予想以上に多くのステップが必要だったが、最終結果は完全な球のように見え、望んだテクスチャ・シェーダ操作をサポートする
- Substance Designer graph 1 つで表面テクスチャを作成でき、出力物に追加の後処理は不要である
- アニメーションテクスチャも同じ方法で扱える
- より小さなテクスチャサイズを使って性能を節約できる
- 必要なだけ tiling を調整できる
- 結果映像は Video 3 で確認できる
1件のコメント
Hacker News の意見
投稿者はキューブマップを早々に除外しすぎているように見える。個人プロジェクトで動的なガス惑星をレンダリングしたときに使ってみたが、最も単純な解法だった
キューブマップを使ってもメモリが6倍になるわけではなく、大きな長方形テクスチャ1枚を小さな長方形の面6枚に分けるだけなので、全体のテクスチャのディテールは同じ
利点は、極での潰れを気にしなくてよく、3次元または4次元のノイズ関数で、テクスチャのアニメーション/歪ませ用のシームレスな流れ場を簡単に作れること
https://www.junkship.net/News/2016/06/09/jupiter-jazz
各半球は円盤に投影されるが、テクスチャを正方形の境界まで埋め、各半球の一部を反対側のテクスチャの隅に重複させられる
円盤の中心と端の間に1:2のスケール差があるため、最小ディテール基準ではピクセルの無駄があるとも言えるが、等角投影なので急な遠近で対象ピクセルの色をサンプリングする方法がずっと扱いやすくなり、順方向/逆方向の計算も投影点ごとに除算1回と加算・乗算数回で済むため、キューブマップの中心投影よりも安い
スケール変化と隅のピクセルの無駄をさらに減らしつつ、概念的に難しすぎない等角方式が必要なら、互いに直交して少し重なるメルカトル投影2つを使い、野球ボールの革2枚のように球を覆うことができる
各ピースは長方形テクスチャにでき、NOAA の論文の中には、地球の天気シミュレーションにおける微分方程式グリッドにこのアプローチを提案しているものもある
私が知る中で最もピクセル効率のよい投影は、球を八面体に分けたうえで、各八分円を「球面面積座標」ベースの六角形ピクセルグリッドで覆う方式
各八分円は通常の正方形ピクセル画像では半分の正方形、つまり45-45-90の直角三角形として表現でき、https://observablehq.com/@jrus/sac-quincuncial のような結果になり、https://observablehq.com/@jrus/sphere-resample のような六角形グリッドも使える
ただし境界線を越えてサンプリングする必要があるときの細部の処理は、立体射影2つの方式よりはるかに面倒で、継ぎ目アーティファクトも生じ得る
数学はさらに必要になるが、別の理由で表面をさらに細分する予定がないなら、普通はそこまでの価値はない
極でのテクスチャの潰れは、実は表面全体に存在する歪みが極端に現れた形
通常は極でしか目立たないが、球を三角形で適応的に細分すると、細分レベルが変わるときに歪みも一緒に変わるため、ほかの場所でも見えることがある
問題は、球が四角形に分けられ、各四角形が UV 空間では同じ面積を持つ三角形2つとして表される一方、3D 空間では片方の三角形、特に水平な辺が極に近い側の三角形のほうが小さいことにある
それでも UV は三角形内部で線形補間されるので、テクスチャの半分は縮み、半分は伸びる
極では片方の三角形の3D面積が実際に0になり、テクスチャの半分だけがレンダリングされ、三角形同士の継ぎ目がはっきりする
正しい解法は、頂点単位の線形補間ではなく、ピクセルシェーダーでピクセルごとに UV 座標を計算することで、適切に行えば極も継ぎ目なく処理できる
たとえば「真北」の極を持つ球をレンダリングするとき、横から見るなら真北側のレンダリング極を使い、真北に近い側から見るなら 0',0’ の赤道レンダリングを使う、という具合
ディスプレイスメントマッピングをまた見直したくなった
投稿者が解こうとしている問題の代替にはならないだろうが、より単純でかなり面白い
約25年前、SoundJam 向けに「Eclipse」という音楽ビジュアライザーを作ったことがあり、入力は可聴周波数範囲のレベル配列と左右チャンネルだった
目標はコロナ放出のある日食中の太陽のような見た目で、音楽データがその「放出物」になる
データの周波数が、太陽円盤の周囲のどこに現れるかを決めていた
時間が経つと放出物は太陽から遠ざかり、「冷えて」黒へと消えていき、強い信号は白から始まって弱くなるにつれ黄色、オレンジ、赤、茶色になった
放出物が黒へ消えるまでの時間を保持できるだけの大きさのリングバッファに、音声データ値の配列を維持する必要があった
「Eclipse」全体と放出物の表示は結局ディスプレイスメントマップであり、各ピクセル値が音声レベルバッファのオフセットになるビットマップを事前計算しておいた
太陽表面に近いピクセルは新しく入ってくるデータのオフセットを、外側のピクセルはまもなく期限切れになるバッファ末尾側のオフセットを持っていた
円形・放射状の放出をデカルト座標にマッピングしようとすると、変位値の生成に少し数学が必要だった
メインループは、新しい音声値を受け取って最古の値を上書きした後、ディスプレイスメントマップを行・列で走査し、対応する音声データを取得して固定パレット色にマッピングし、表示バッファに入れる、というものだった
ほかのビジュアライザーほど派手ではなかったが、落ち着いた美しさがあり、音楽データをかなりよく反映していた
後のビジュアライザーのように、無音を入れてもじっとしていられない感じとは違っていた
記事は素晴らしいが、下へスクロールするほどシェーダーを大量に読み込むので、非常に高性能なコンピュータでないとブラウザがほとんど固まることがある
「リアリティ」を人々がどれほど違って解釈するのかが興味深い
リソースがはるかに限られていた時代のゲームでは、この違いが特に顕著で、四角くピクセル化された低パレットのグラフィックだけでも、今日の大作予算ゲームより没入感を与えるものがあった
だからより能動的に没入できたのだと思う
似たアプローチを扱ったこの記事を思い出す: https://bgolus.medium.com/rendering-a-sphere-on-a-quad-13c92...
GPU や 3D ライブラリには、円の各水平ラインを横に走査しながら、テクスチャの X,Y を球面上の「3D」位置にマッピングする関数はないのだろうか?
球は円で、円を描くアルゴリズムは単純なのに、三角形用に 100 万個の頂点を回転・投影するのは、ものすごいリソースの無駄に見える
水平に走査して 1 行下へ進み続ければ、すべての行を描ける
80 年代後半に Second Reality の拡大レンズのようなものを事前計算するために、こういうことをやっていた記憶がある
レンダリングパイプライン全体は、すでに線形補間を処理するように作られている
シェーダープログラム 1 つと三角形 1 個だけでも、完全にアンチエイリアスされた球を描ける
GPU でレンダリングするなら、見かけの複雑さとは裏腹に、この記事のアプローチはかなり良さそうに見える
GPU ベースのレンダリング構造を扱った優れた連載がある: https://fgiesen.wordpress.com/2011/07/09/a-trip-through-the-...
第 6 回はラスタライズを扱っている
背景上の 2D ピクセルシェーダーのインポスター惑星 は、プロシージャルな宇宙のように惑星が多い作業で本当に大きな助けになる
メモリと GPU 帯域幅の都合で、球面キューブベースの惑星は 1 インスタンスに制限し、近くの天体は背景技法で描く
宇宙空間では、2 つの天体の中間あたりに、両方がインポスターとしてレンダリングされる距離があり、その後は最も近い天体を球面四分木方式で処理する
完璧ではないが、錯視はほとんど非の打ちどころがない
また平面上にあるので、遠くの銀河や天体に対してカメラレンズの軽い物理効果を入れるのも簡単になる
たとえば月が昇るときに実際より大きく見えたり、遠方の銀河が重力で曲がって見えたりするような効果だ
ガス惑星と関連ページが気に入った: https://emildziewanowski.com/flowfields/
イコスフィア は、はるかになめらかに展開でき、頂点位置と要素サイズも規則的だ
展開は些細ではないが、可能ではある