空を見上げると、雲がゆっくり流れていく。その動きを3Dで再現したくて、前回(midori50)ではテクスチャで雲を描いた。見た目は良かったが、球面に貼った画像のつなぎ目がどうしても目についた。UV座標(テクスチャの2次元の座標)は球の上下で一周してつながるため、その境界でノイズが不連続になる。そこで、3Dのワールド座標でノイズを計算する方式に切り替えることにした。空間のどの点でも同じ計算になるので、継ぎ目が出ない。 作ったもの 3層の球体雲が空間に広がる仕組みだ。内側から見上げる形なので、球の内側に描画する(Three.js では side: THREE.BackSide)。3D座標で密度を出すので球の継ぎ目が目立たない。層ごとに違う速度・方向で動かし、光の散乱で明るさをつけている。全方位表示にしたので、カメラを回すとどの角度からも雲が見える。 最初は1層だけ作った。単純で軽いが、奥行きが足りない。3層にして、それぞれに異なるアニメーション速度と方向ベクトルを渡した。内側はゆっくり水平寄り、外側は速く斜めに。そうしたら、空全体に広がる感じになった。 3層の雲を全画面で見る 3D座標でノイズを出す理由 テクスチャ(UV)だと、球の極や経線で座標がつながる部分に継ぎ目が出る。3Dのワールド座標(頂点をワールド空間に変換した vWorldPosition)でノイズを計算すれば、どの頂点でも入力が一意になるので継ぎ目が消える。 頂点シェーダーでは vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz で座標を渡す。フラグメントシェーダーでは、この座標を noise3D に渡して密度を求める。noise3D は、整数格子の8頂点の擬似乱数を smoothstep 補間(f = f * f * (3.0 - 2.0 * f))で滑らかに混ぜる。格子の識別には n = i.x + i.y * 57.0 + 113.0 * i.z のようなハッシュを使っている。 📝 NOTE UV(u,v)はテクスチャの横・縦の座標。球面では北極・南極で一点に集まり、経度0と360でつながるため、その境界でノイズが途切れやすい。 2Dノイズ(UVベース)だと継ぎ目がはっきり見える。3Dに切り替えると消えて、自然な雲になる。 FBM(フラクタルブラウニアンモーション)で雲の密度を決める FBMは、周波数を2倍ずつ上げながらノイズを何段階も重ねる手法。低い周波数で大まかな形、高い周波数で細かさを足す。雲のメイン形状用に 3DのFBM を10オクターブ、ワープ用に 2DのFBM を6オクターブにした。 試したのは 6 / 8 / 10 オクターブ。6だと粗く、8だとまだ物足りない。10で細かさと負荷のバランスが取れた。各オクターブでは amplitude *= persistence(persistence = 0.5)で振幅を半分にし、maxValue で正規化して 0〜1 に収めている。 GLSL17行コピー// 3D FBM(シェーダー内) float fbm3D(vec3 p) { float total = 0.0; float persistence = 0.5; float amplitude = 0.5; float frequency = 1.0; float maxValue = 0.0; const int octaves = 10; for(int i = 0; i < octaves; i++) { total += noise3D(p * frequency) * amplitude; maxValue += amplitude; frequency *= 2.0; amplitude *= persistence; } return total / maxValue; } マルチレイヤーFBMとスケールの試行錯誤 雲の密度は、メイン・詳細・微細の3つのFBMを重ねて出す。式は mainFbm * 0.6 + detailFbm * 0.3 + fineFbm * 0.1。メインは空間スケール 0.8、詳細は 2.5(メインの結果を 0.5 かけてオフセットに足す)、微細は 5.0(詳細を 0.3 かけて足す)。層ごとのアニメーションは uLayerAnimationDirection * uLayerAnimationSpeed * time でオフセットに加えている。 スケールは worldPos * 0.01 にしている。ここでかなりはまった。最初は 0.1 にしたら雲が粗すぎた。0.001 にすると細かすぎて重くなった。0.01 で、見た目とパフォーマンスが両立した。 重みの 0.6 / 0.3 / 0.1 も試行錯誤の結果だ。均等(0.33ずつ)にすると細かい成分が強く出て、雲がざらついた。メインを 0.6 に上げてから落ち着いた。 ワーピングで流れを足す 2DのFBMで座標をずらす「ワープ」を入れている。warpCoord に worldPos.xy 0.005 + worldPos.z 0.005 を使い、時間でずらした2Dノイズをベクトルにして、3Dノイズの入力に vec3(warp, 0.0) を足す。最後に mix(density, warped, 0.3) で元の密度と3割混ぜた。0.5 だと歪みが強すぎて不自然、0.1 だと効きが弱い。0.3 がちょうどよかった。 メイン・詳細・細かいを切り替えて見ると、それぞれの役割が分かる。 光の散乱(フォワード・バックワード) 雲は光を散乱させる。フォワード散乱は光が進む向きに散る効果、バックワード散乱は後ろに散る効果。視点方向と光方向の内積 viewLightDot で切り替えている。1に近いとフォワード、-1に近いとバックワードだ。 フォワード: pow(max(0.0, viewLightDot), 1.5) × 係数 1.2 バックワード: pow(max(0.0, -viewLightDot), 1.2) × 0.4 smoothstep(-0.3, 0.7, viewLightDot) でどちらを効かせるかブレンドしている。 散乱の強さは pow(density, 1.8) * 2.5。指数 1.5 だと弱く感じたので 1.8 にした。係数 2.5 も試した結果で、1.0 だと暗く、3.0 だと白飛びする。2.5 で自然な明るさになった。 散乱パラメータの比較 | 係数 | 印象 | | 1.0 | 雲が暗い | | 2.5 | 自然(採用) | | 3.0 | 白飛びする | 光源の X/Y/Z を変えると、散乱の見え方が変わる。 3層の配置とアニメーション 内側・中間・外側の3層を、基本半径 500、層間隔 80 で並べた。つまり半径 500 / 580 / 660 の球。間隔 50 だと層が重なりすぎて一塊に、200 だと離れすぎて薄く見えた。80 が奥行きと密度のバランスが良かった。 各層には 速度ベクトル と 方向ベクトル(正規化) を渡している。内側は (0.8, 0.5, 0.6) と (1, 0.3, 0.5)、中間は (1.2, 0.8, 1.0) と (0.5, 1.0, 0.3)、外側は (1.5, 1.2, 1.3) と (0.7, 0.5, 1.0)。最初は全層同じ速度にしていたら、同調して単調になった。ずらしてから動きに奥行きが出た。加えて、中間・外側には Y軸まわりの回転 を layerTime * (0.2 + layerIndex * 0.1) で入れている。層ごとに時間オフセット layer * 10.0 も足してある。 球は SphereGeometry(radius, 128, 128) で生成。128は解像度で、64だと角ばり、256だと重い。外側の層ほど密度を少し落とすため、density *= (1.0 - layerIndex * 0.15) をかけている。 内側だけ・中間だけ・外側だけ・全て、と切り替えられる。 アルファと全方位表示の調整 透明度は smoothstep(0.2, 0.65, density) で密度から計算し、pow(alpha, 1.2) でエッジを少しきれいにしている。全方位表示のときは alpha = max(alpha, 0.1) にした。0.05 だと薄すぎて見えにくく、0.1 でどの角度からも雲がはっきり見える。上半球だけの表示のときは、視点が水平に近いほどフェードする viewEffect をかけている(viewFade で強さを指定)。 光源と層間隔のUI 光源方向は X / Y / Z のスライダー(-2〜2、0.1刻み)、層間隔は 20〜200 で 5 刻み。スライダーを動かすと全層の uniform(シェーダーに渡す値)をその場で更新する。層間隔を変えたときは、内側はスケール1のまま、中間・外側のメッシュだけ scale を変えている。目標半径を baseRadius + index * layerSpacing とし、初期半径(間隔80で作ったとき)との比でスケールを計算している。 デモ6で光源・層間隔をいじる 試したこと(具体例) 光源を反転 (0.5, 1.0, 0.3) から (-0.5, 1.0, -0.3) にすると、明るい部分と暗い部分が入れ替わった。散乱の向きが正しく効いている確認になった。 層間隔 80 → 40 / 200 40 だと層が近づいて重なりが強く、200 だと離れすぎて薄く広がった。80 が、奥行きと密度のバランスでいちばんしっくりきた。 全方位表示 displayWholeSphere = true にして、アルファの最小を 0.1 にした。下を向いても雲が消えず、空に包まれる感じになる。 2Dノイズ(UV) 球面の極・経度で不連続 テクスチャ座標に依存 3Dノイズ(ワールド座標) どの方向でも連続 空間の位置だけで決まる まとめ 3D座標のノイズで球面の継ぎ目をなくした(UVではなくワールド座標で計算)。 マルチレイヤーFBM(メイン0.6・詳細0.3・微細0.1)とワーピングで、雲の密度を自然にした。 フォワード・バックワード散乱と密度ベースの強さ(指数1.8、係数2.5)でライティングした。 3層を別速度・別方向・時間オフセットで動かし、奥行きを出した。 光源方向と層間隔をスライダーで変えられるようにした。層間隔変更時は中間・外側のスケールだけを更新している。 さらに深く学ぶには Three.js 公式 - 3Dの基礎 WebGL Fundamentals - WebGLの解説 The Book of Shaders - シェーダー入門 FBM (The Book of Shaders) - FBMの説明 midori50: テクスチャの雲 midori52: 高度な雲
• 記事タイトル 目次 記事を読み込み中... 🔗 関連する実行デモ この記事に関連する実行デモは旧サイトでご覧いただけます: 旧サイトのデモを見る この記事をシェア Twitter Facebook はてブ URLコピー 埋め込み 記事を埋め込む × 以下のコードをコピーして、あなたのサイトに貼り付けてください。 コードをコピー 関連記事