[Next.js #41] Procedural Night Lights on Storm Planet — 昼夜境界を動かし、文明の灯りが浮かぶ惑星を描く

はじめに

前回の Storm Planet では、球体地形・雲球・雷の発光演出を組み合わせることで、「荒れた気候を持つ惑星」の表現を作りました。 雲が流れ、雷が走り、昼側だけでも十分に表情はありましたが、惑星として見た時にまだ足りないものがありました。夜の顔です。

現実の地球を宇宙から見ると、昼の地形だけでなく、夜の都市灯が文明の存在そのものを語ります。海岸線や大陸の輪郭とは別に、人間が作った光の網が浮かび上がることで、単なる球体ではなく「誰かが暮らしている惑星」へと印象が変わります。 そこで今回は、今朝の Noise 入門 #37 で実装した Procedural Night Lights の考え方をベースに、前回の Storm Planet へ 夜景表現(Night Lights) を移植しました。

今回の主役は、単なる発光ノイズではありません。

昼と夜を分ける境界線(Terminator) をシェーダー内で計算し、その夜側にだけ都市灯を浮かび上がらせます。さらに、固定値だった lightDir を uLightDir として uniform 化し、GUI や時間によって太陽方向を動かせるようにしました。これにより、惑星が回っているだけではなく、昼夜境界そのものが球面上を移動し、文明の灯りが押し寄せたり引いたりする、より“生きた惑星”らしい表現へ発展しました。

動画(YouTube):

動画(PC):

Storm Planet に Night Lights を追加する

今回の実装は、前回の記事で構築した惑星シーンを土台にしています。 地表用の Sphere Geometry、外側に重ねた Cloud Sphere、そして大気や雷の演出といった構成はそのまま維持しつつ、地表フラグメントシェーダの最終色合成段階に「夜景レイヤ」を追加する方針です。

このアプローチの利点は、既存の地形ノイズや雲影、昼側のライティングを壊さずに拡張できることです。 昼の世界観はそのままに、夜側にだけ別の情報層を追加する。つまり今回は「地形を作り直す」のではなく、惑星にもう一つの顔を与える実装になります。

また、夜景を最初から画像テクスチャで貼るのではなく、あくまで プロシージャル に組み立てることで、これまでの Noise 入門シリーズや自作シェーダー群と思想を揃えています。 見た目だけでなく、実装そのものが「ノイズで世界を育てる」流れの延長線上にあります。

昼夜境界(Terminator)を dot(normal, lightDir) で求める

昼夜を分ける基本はシンプルです。 各ピクセルの法線 normal と、太陽方向を表す lightDir の内積を取れば、その地点がどれだけ太陽に向いているかが分かります。

float daylight = dot(normalize(vNormal), normalize(uLightDir));

この daylight は、値が大きいほど昼、0 付近が境界、マイナス側が夜になります。 球体における太陽光の当たり方はこの一行で概ね表現できるため、惑星表現では非常に強力です。 しかもこれは単なる明暗処理に留まりません。後段で 夜景をどこに出すか、雲影をどちらへ伸ばすか、薄明(twilight)をどこまで広げるか といった判断の基準にもなります。

これまで lightDir を固定ベクトルにしていた場合、惑星が自転していても「太陽の演出そのもの」は止まったままでした。 それでもデモとしては成立しますが、今回はここを一段押し進めて、昼夜境界自体を動かすことで、作品の印象を大きく変えています。

smoothstep で twilight を作る

現実の惑星には、昼と夜がカチッと切り替わる境界線は存在しません。 夕方・朝方のような、薄く光が滲む帯があります。これをシェーダーで作る時に便利なのが smoothstep です。

たとえば、daylight の値をそのまま二値化すると、昼夜境界が硬く不自然になります。 そこで smoothstep を使い、境界付近だけをなめらかに補間することで、薄明の幅 をデザインできます。

float twilight = smoothstep(-0.15, 0.15, daylight);

このようにして得られる値は、昼側へ完全に寄る前の「中間地帯」を表現できます。 ここで重要なのは、smoothstep の閾値を少し広げたり狭めたりするだけで、惑星の見え方が大きく変わることです。

  • 幅を狭くすると、宇宙空間らしいシャープな境界になる
  • 幅を広くすると、大気の厚みや夕焼け感が強くなる
  • 少し負側へずらすと、夜景が早めに立ち上がる
  • 正側へ寄せると、夜景がより深い夜だけに限定される

今回のような「文明の灯り」を見せたい回では、昼夜をきっぱり切るよりも、境界の揺らぎや滲み を残した方が、夜景の出現にドラマが生まれます。

Voronoi Noise で都市網を作る

夜景表現の核になるのが、都市の灯りをどう配置するか です。 単純なランダムノイズをそのまま発光させると、どうしても「砂粒が光っている」ような見え方になりがちです。 そこで今回は、Voronoi Noise を使って、都市が集積し、道路やネットワークのような繋がりを感じるパターンを作ります。

Voronoi 系のパターンは、セル境界や点の分布から「人工物っぽさ」を出しやすいのが特徴です。 山や雲のような自然地形ではなく、区画・街区・網目 の印象を与えられるため、文明の気配を乗せるには非常に相性が良いです。

実装としては、球体上の UV や座標を元に Voronoi パターンを生成し、その結果をそのまま使うのではなく、

  • 大きい都市コア
  • 周囲へ伸びる細い道路状の線
  • ランダムなノイズによる明滅差
  • 海や山を想像させる“灯りのない余白”

といった複数の要素を混ぜることで、単調さを避けています。

つまり、重要なのは「Voronoi を使った」ことではなく、Voronoi を文明の痕跡に読み替える ことです。 自然ノイズで地形を作るのと同じように、人工ノイズで都市圏を作る。ここが今回の面白いところです。

夜側だけに文明の灯りを浮かび上がらせる

都市灯パターンができても、それを惑星全体に均一に載せてしまうと台無しです。 夜景はあくまで 夜側でだけ見えるもの でなければなりません。 そこで daylight から夜側のマスク、つまり nightMask を作ります。

float nightMask = 1.0 - smoothstep(-0.05, 0.2, daylight);

この nightMask に都市灯の強度を掛けることで、昼側では消え、夜側へ行くほど浮かび上がる 挙動になります。 ここでも smoothstep を使うことで、昼夜境界付近でいきなり消えるのではなく、薄明帯ではうっすら、深夜側ではしっかり、という自然な立ち上がりが作れます。

この処理の良いところは、都市灯が単なるエフェクトではなく、惑星の光環境に従属する存在 になることです。 つまり、夜景を足したのではなく、「太陽に照らされる惑星の片側に、文明の光が宿っている」という関係性を作れます。

この段階まで来ると、昼側の地形・雲・雷と、夜側の都市灯が一つの惑星の中に共存し始めます。 嵐の惑星だったものが、ただ荒れているだけではなく、その闇の中に文明が息づいている惑星 へ変わっていきます。

uLightDir を uniform 化して太陽方向を動かす

今回の実装で一番大きい変化はここです。 これまで lightDir = vec3(1.0, 1.0, 1.0) のように固定していた太陽方向を、uLightDir として shader に渡すようにしました。

uniform vec3 uLightDir;

JS 側では、方位角(Azimuth)と高度(Elevation)から 3D ベクトルを作り、毎フレーム uniforms.uLightDir.value を更新します。

const x = Math.cos(el) * Math.cos(az);
const y = Math.sin(el);
const z = Math.cos(el) * Math.sin(az);

uniforms.uLightDir.value.set(x, y, z).normalize();

これによって何が変わるか。 単に“光源が動く”だけではありません。

  • 昼夜境界が球面上を移動する
  • 夜景が点灯する領域そのものが変わる
  • 雲影や明暗の向きが太陽方向に追従する
  • 「この惑星はいま何時なのか」を感じられる

つまり、太陽方向を動かせるようにした時点で、惑星は静的なデモから、時間を持つ存在 に変わります。

今回ここまで入れたことで、今後は GUI で手動に動かすだけでなく、時刻や公転を模した自動制御にも繋げやすくなりました。 昼夜境界が動くというのは、見た目以上に大きい意味を持ちます。球体が回るのではなく、世界の光そのものが巡る ようになるからです。

GUI と自動周回で “生きた惑星” にする

uLightDir を可動化したことで、GUI 側にも太陽制御パラメータを追加できるようになりました。

  • sunAzimuth
  • sunElevation
  • sunAutoOrbit
  • sunOrbitSpeed

これにより、デモとしての触り心地も大きく変わります。 以前は完成した見た目を眺めるだけだったものが、今回は 太陽の位置を変えると惑星の表情そのものが変わる ため、インタラクティブな観察対象になります。

手動で sunAzimuth を回すと、夜景帯が地表を滑るように移動し、薄明のリングが球面上を這っていきます。 sunElevation を変えると、極地方への光の入り方や夜側の面積が変わり、同じ惑星でもまるで別の時間帯や季節のような見え方になります。 さらに sunAutoOrbit を有効にすると、境界線そのものが少しずつ巡回し、止まっていた惑星に時間の流れが宿る のが分かります。

ここで面白いのは、回っているのは planet の rotation.y だけではないという点です。 本当に動いているのは、惑星に当たる光の論理 です。 この差は大きく、見ている側の印象も「ただの回転デモ」から「宇宙に浮かぶ環境シミュレーション」へ近づきます。

雷・雲・夜景が共存することで惑星の物語性が増す

前回の Storm Planet は、雲と雷によって「自然の暴力」を持つ惑星でした。 今回そこへ夜景が入ったことで、ただの嵐の球体ではなくなりました。 暗い側に人工の灯りがあることで、見る側は無意識にこう考えます。

この惑星には街がある。 文明がある。 嵐の中でも誰かが生きている。

これは単なるビジュアル追加以上の効果です。 ノイズで地形を作ると、世界は生まれます。 しかし、夜景を載せると、そこに生活圏の気配が生まれます。 自然景観だったものが、一気に SF 的な物語を帯び始めます。

しかも今回は、雷・雲・昼夜境界・夜景がそれぞれバラバラのレイヤではなく、同じ lightDir と球面法線を基準に結び付いています。 この「共通の論理で複数の演出が支えられている」ことが、全体の説得力を底上げしています。

今回の実装で見えてきた次の拡張

今回の段階で、昼夜境界を動かしながら夜景を見せる、という目的は十分達成できました。 ただ、ここまで来ると次にやりたくなることもはっきりしてきます。

一つは、時刻ベースの制御 です。 今は Azimuth / Elevation を直接いじっていますが、次は timeOfDay のような値から太陽方向を求めれば、UI もより直感的になります。 朝・昼・夕・夜という概念で惑星を操作できるようになれば、表現としてさらに強くなります。

もう一つは、季節や自転軸傾斜 の導入です。 axialTilt や seasonPhase を持たせれば、極地方の光量差や昼の長さも変えられるようになります。 そこまで行けば、単なる発光球ではなく、かなり本格的な「惑星の時空間表現」へ近づいていきます。

そして視覚面では、夜景の分布をもっと地形と関連づける余地もあります。 たとえば高地や海洋部では灯りを減らし、平野や沿岸部で密度を高めれば、より現実らしい都市圏が作れます。 今はまず「文明の灯りが生える」段階ですが、その次には「どこに文明が育つか」という地理的文脈まで入れられます。

まとめ

今回は、前回の Storm Planet を土台にして、Procedural Night Lights を Three.js / GLSL の惑星表現へ移植しました。 dot(normal, lightDir) による昼夜判定、smoothstep による twilight 制御、Voronoi Noise による都市網、そして nightMask による夜側限定の発光。この組み合わせにより、単なるノイズ球ではなく、昼と夜、自然と文明が共存する惑星 を描けるようになりました。

さらに、固定だった lightDir を uLightDir として uniform 化したことで、太陽方向を GUI や時間で動かせるようになり、昼夜境界(Terminator)そのものが作品の主役になりました。 これによって惑星は、回転するだけの球体から、時間と環境が巡る存在 へ一歩進んだと感じています。

ノイズで山を作る。 雲を流す。 雷を走らせる。 そして夜にだけ文明の灯りを浮かべる。

こうして少しずつ層を重ねていくと、球体一つでも「世界」になっていくのが面白いところです。 次は、この太陽方向の可動化をさらに進めて、時刻・季節・軸傾斜まで含めた、より本格的な惑星の時間表現にも踏み込んでみたいと思います。

Storm Planet は、雷を得て、夜景を得て、ようやく「そこに誰かが住んでいそうな惑星」になってきました。次はこの世界に、さらに時間そのものを流し込んでいきます。