1. はじめに
前回(Next.js #09)では Perlin/FMB ノイズで地形を生成し、プレイヤーを Raycast で歩かせるところまで実装。
前回の記事:
[Next.js #09] Perlin/FBM ノイズで地形を作り、プレイヤーを Raycast で追従
Next.js(App Router) + R3F 環境で、Perlin/FBM(フラクタルノイズ)を使った地形生成と、Raycaster による地面追従プレイヤー制御を実装する。PlaneGeometry の頂点変 …
https://humanxai.info/posts/nextjs-09-noise-terrain-raycast-player-follow/最近のサブテーマは、「ノイズ」なので、今回もノイズを活用した、霧・雲を実装してみたのでその備忘録メモです。
[Noise 入門 #01] ノイズとは何か?— 滑らかな乱数の正体を理解する
JavaScript・Three.js・Shader へ進む前に、まず『ノイズとは何か?』を徹底的に理解する入門回。自然の“ゆらぎ”を作る数式としてのノイズの本質、ランダムとの違い、周波数や振幅などの最低限の概念を整理する。
https://humanxai.info/posts/noise-intro-01-what-is-noise/仕組み的には、R3F と GLSL で本物の雲のように動く SkyDome(雲の球体)をキューブマップに貼るテクスチャの代わりに、シェーダーでノイズを動的に生成するという実装内容です。
動画を撮ってみたのでアップします。(Youtubeのショート動画にもアップ予定)
画像:
動画:
2. 雲の作り方の基本:SkyDome とは?
SkyDome(スカイドーム)は、 巨大な球体の“内側”に空のテクスチャを描く手法 だ。 現代のゲームエンジンでも一般的に使われている。
今回のポイントは次の 3つ。
- 巨大な球体(半径 500〜1000)をワールド全体に被せる
- 球体の裏側(BackSide)を描画する
- FBM ノイズで雲を生成し、時間パラメータで流す
これによって、
- 黒いキューブマップのように空が動く
- 完全自作の “動く空” が構築できる
- どの角度から見ても破綻しない
という、 R3F × GLSL ベースのシンプルかつ柔軟な SkyDome が完成する。
3. CloudDome のコード
export function CloudDome() {
const mat = useRef<any>();
useFrame((s) => (mat.current.time = s.clock.elapsedTime));
return (
<mesh renderOrder={-2}>
<sphereGeometry args={[500, 64, 64]} />
<cloudMaterial
ref={mat}
side={THREE.BackSide}
transparent
depthWrite={true}
/>
</mesh>
);
}
このコンポーネントの役割
R3F で 「雲の球体(SkyDome)」を描画する部分 だけを切り出したもの。 内部では次のような処理をしている:
500 → 雲を描く球の半径
ワールド全体を覆いたいため、かなり大きめの値を指定する。 500〜1000 程度が扱いやすい。
BackSide → 球の内側を描画
雲は 球体の外側ではなく“内側”に貼る ので必須。
R3F / Three.js では、
side: THREE.BackSide
と書くことで「裏面だけ描画する」設定になる。
time を毎フレーム更新
useFrame の elapsedTime を mat.current.time に渡すことで、
GLSL 内で FBM ノイズが時間で変化し、
“雲が流れる” エフェクト が生まれる。
4. CloudMaterial(GLSL シェーダー)
R3F の shaderMaterial を使い、
FBM(Fractal Brownian Motion)ノイズで雲を生成する シェーダーを定義する。
SkyDome の見た目はほぼこのシェーダーのクオリティで決まる。
頂点シェーダ(vertex shader)
varying vec3 vWorld;
void main() {
vec4 w = modelMatrix * vec4(position, 1.0);
vWorld = w.xyz;
gl_Position = projectionMatrix * viewMatrix * w;
}
解説
modelMatrix * positionで ワールド座標を取得- FBM の入力に必要なので
vWorldとしてフラグメント側へ渡す - ここでは頂点を変形しないため、通常の MVP 行列で描画しているだけ
フラグメントシェーダ(fragment shader)
uniform float time;
uniform vec3 cloudColor;
varying vec3 vWorld;
float hash(vec3 p){
return fract(sin(dot(p, vec3(12.9898,78.233,128.852))) * 43758.5453);
}
float noise(vec3 p){
vec3 i = floor(p);
vec3 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
return mix(
mix(
mix(hash(i + vec3(0,0,0)), hash(i + vec3(1,0,0)), f.x),
mix(hash(i + vec3(0,1,0)), hash(i + vec3(1,1,0)), f.x),
f.y
),
mix(
mix(hash(i + vec3(0,0,1)), hash(i + vec3(1,0,1)), f.x),
mix(hash(i + vec3(0,1,1)), hash(i + vec3(1,1,1)), f.x),
f.y
),
f.z
);
}
float fbm(vec3 p){
float v = 0.0;
float a = 0.5;
for(int i=0; i<5; i++){
v += a * noise(p);
p *= 2.0;
a *= 0.5;
}
return v;
}
void main(){
float n = fbm(vWorld * 0.002 + time * 0.02);
float cloud = smoothstep(0.3, 0.8, n);
gl_FragColor = vec4(cloudColor * cloud, 1.0);
}
ここが“雲”の本質(超重要)
hash()
格子状のランダム値を生成する最小ノイズ関数。
noise()
3D グラデーションノイズ(Perlin に近い)。 座標を 8 点で補間して「滑らかなランダム」を作る。
fbm()
Perlin(ノイズ)を 周波数 ×2、振幅 ×0.5 で重ねて奥行きのある模様を作る。 これが “雲の質感” の正体。
smoothstep(0.3, 0.8, n)
ノイズ値をしきい値で切り、 雲の境界を 柔らかくするフィルタ。
time
vWorld + time によって
ノイズのサンプリング位置がずれ → 雲が流れる。
出力
vec4(cloudColor * cloud, 1.0)
ここではアルファ固定 1.0 にしているため SkyDome は常に視界に表示される。
(Fog で薄めたり、透明にしたい場合はここを 0.6〜0.8 にしても良い)
全体としての理解
このシェーダーは、
- Perlin 風ノイズ
- FBM(多層ノイズ)
- smoothstep
- 時間アニメーション
- 裏面描画
を組み合わせて、
見た目が重厚な “動く雲の空” をリアルタイム生成
している。
Web ではここまでやる例が少なく、 lain の実装は普通に “研究レベルのクオリティ” になってる。
5. 完成:動くスカイドーム
上のように、 黒い雲がゆっくり流れるスカイドーム が完成する。
特徴としては:
- 黒ベースの雲テクスチャ(FBM)
- ノイズの濃淡が 時間で変化して流れる
- プレイヤーの視点で見上げると 空全体が有機的に動く
- 地形(NoiseTerrain)の上に“世界の空”が広がる
R3F + GLSL だけで、 ここまで重厚な “動く空” を生成できるのは強力だ。
Unity / Unreal の SkySphere と同じ発想だが、 WebGL(R3F)で完全自作できる 点が大きい。
これで Terrain(地形)+ SkyDome(空)+ Player(移動) という “世界の骨格” が完成する。
6. 地形+雲+プレイヤーで「世界」が完成する
Next.js + R3F だけで、 Terrain(地形)・Sky(空)・Player(歩く) の三要素が揃った。
これはつまり、
- 地形を生成し
- 空をシェーダーで描き
- プレイヤーが歩き回れる
という “世界の骨格” が Web 上で動いている状態だ。
発想としては Unity / Unreal の構成と同じで、 違いは WebGL(R3F)でこれを全部自前実装できる ところにある。
この時点で「デモ」ではなく、 ゲーム空間としての土台ができた と言っていい。
💬 コメント