[Shader 入門 #05] 波・水面・炎・発光を作る:sin / noise / depth / Fresnel(Three.js & Unity)

はじめに

第5回のテーマ

  • 水面(SimpleWater)
  • 炎(noise)
  • 発光(emission + fresnel)
  • 深度を使った透明度
  • 法線で反射を調整

前回の記事:

1. はじめに:なぜ“テーマ別”が最短の成長ルートなのか

シェーダーの学習は、理論から入るよりも
「作品でよく使う表現」を真似するほうが圧倒的に早い。

ゲームでも映像でも、初心者が最初に欲しくなるのは次の4つ。

  • 波(水面) → sin
  • 炎 → noise(Perlin / Simplex)
  • 透明度(水やガラス) → depth
  • 光(発光・リムライト) → 法線 / fresnel

実は、この4つを理解するだけで
水・炎・煙・ガラス・発光体・オーラ・魔法エフェクト
といった、ゲームの VFX の“8割”が作れる。

三角関数やノイズ、深度、法線といった基本要素は
Three.js(GLSL)でも、Unity(HLSL / URP)でも共通している。

つまり、

テーマ別に 4 つだけ押さえた方が
シェーダーは最速で上達する。

本記事では、この“4 本柱”を両エンジンで実装しながら理解していく。

2. 波(水面)シェーダー:sin波だけで作れる

水面の基本は 「頂点を sin 波で上下に揺らす」。 たったこれだけで、シェーダー初心者がいきなり“水っぽい動き”を作れる。

Three.js(GLSL)でも Unity(HLSL)でもやることは同じで、

  • 時間 time を使って
  • sin 波を計算し
  • 頂点の高さ(y座標)に足す

この 3 ステップだけ。


GLSL(Three.js)

Three.js の場合は RawShaderMaterial を使い、 GLSL の Vertex Shader 内で高さを揺らす。

// vertex shader
uniform float u_time;
varying vec2 vUv;

void main() {
    vUv = uv;

    vec3 pos = position;
    pos.y += sin(pos.x * 4.0 + u_time * 2.0) * 0.1;

    gl_Position = projectionMatrix *
                  modelViewMatrix *
                  vec4(pos, 1.0);
}

ポイント:

  • pos.x * 周波数 + u_time * 速度
    • 0.1 の振幅を調整
  • vUv = uv; を Fragment に渡す

Fragment shader 例(色変化)

varying vec2 vUv;

void main() {
    gl_FragColor = vec4(vUv, 1.0, 1.0);
}

→ 単純だが「動く水」の感覚がつかめる。


HLSL(Unity URP)

Unity でも Vertex シェーダーで高さを揺らす。 URPでは ShaderLab + HLSL の組み合わせになる。

// 頂点シェーダー
float4 vert(appdata v) : SV_POSITION
{
    float3 pos = v.vertex.xyz;

    pos.y += sin(pos.x * 4.0 + _Time.y * 2.0) * 0.1;

    return UnityObjectToClipPos(float4(pos, 1.0));
}

ポイント:

  • _Time.y は秒数(GLSLの u_time と同じ)
  • UnityObjectToClipPos が GLSL の projectionMatrix * modelViewMatrix と等価
  • 波の式は Three.js と 1 対 1 対応

Fragment(色)

fixed4 frag(v2f i) : SV_Target
{
    return fixed4(i.uv, 1, 1);
}

このステップが「SimpleWater の正体」

実際に Unity の SimpleWater(サンプル)を読むと、

  • sin 波を複数足す
  • 色を shallow/deep で補間
  • 法線を再計算
  • reflection probe の色を足す

などがあるが、根幹はここでやった sin の揺れだけ。

つまり、この章を理解すると:

「水面がどう動くか」=数学的には全部 sin の合成

という本質が見えるようになり、
SimpleWater の中身も自然に読めるようになる。

3. 炎(Perlin / Simplex noise)

炎シェーダーの本質は 「ノイズを時間でスクロールさせる」 これだけで “揺らめきのある炎” が実現する。

水面が sin(規則的) なのに対し、 炎は noise(不規則) を使う。

基本ステップは共通:

  1. ノイズ(Perlin / Simplex)を取得
  2. 時間 time を足して“上方向に流れる”動き
  3. ノイズ値で色(yellow → red → black)を補間
  4. 上に行くほど透明にして炎っぽさを出す

GLSL でも HLSL でも全く同じ考え方で書ける。


GLSL(Three.js)

Three.js では glsl-noise(Perlin / Simplex)が使える。

#pragma glslify: noise = require('glsl-noise/simplex/3d')

uniform float u_time;
varying vec2 vUv;

void main() {
    // UV を縦方向にスクロール
    float t = vUv.y + u_time * 0.5;

    // 3D noise(UV + time)
    float n = noise(vec3(vUv * 3.0, t));

    // 炎の色:黄色→赤→黒
    vec3 fireColor =
        mix(
            vec3(1.0, 0.8, 0.2),   // yellow
            vec3(1.0, 0.0, 0.0),   // red
            n
        );

    // 上へ行くほど透明(炎っぽい)
    float alpha = 1.0 - vUv.y;

    gl_FragColor = vec4(fireColor, alpha);
}

ポイント

  • noise(vec3(x, y, t)) で 揺らぎが“流れる”
  • 黄色→赤→黒の mix が “燃える色” を作る
  • 1.0 - vUv.y が上方向の透明(炎の縁が薄くなる)

HLSL(Unity URP)

Unity にはノイズ関数が標準で無いので、
GLSL のノイズ関数をそのまま HLSL にコピペ するのが基本。

最近の Unity シェーダー界隈では普通の手法。

(※記事では短くするため noise 関数は省略し、
「付録で noise 関数コードを添付」がおすすめ)

// noise3D() はGLSLから移植した関数(付録で載せればOK)

float4 frag(v2f i) : SV_Target
{
    float2 uv = i.uv;

    // 縦スクロール + time
    float t = uv.y + _Time.y * 0.5;

    // 3D noise
    float n = noise3D(float3(uv * 3.0, t));

    // 黄色→赤→黒
    float3 fireColor = lerp(
        float3(1.0, 0.8, 0.2),    // yellow
        float3(1.0, 0.0, 0.0),    // red
        n
    );

    // 上方向に透明
    float alpha = 1.0 - uv.y;

    return float4(fireColor, alpha);
}

Unity 版のポイント

  • _Time.y は GLSL の u_time と同じ
  • lerp(a,b,x) は mix(a,b,x) と同じ
  • noise 関数は GLSL → HLSL へコピペで動く(構文ほぼ同じ)

炎の正体は「ノイズを縦に流すだけ」

実際のゲームの炎エフェクトも、

  • 炎の根本が明るい
  • 上に行くほど透明
  • ゆらゆら揺れる
  • 焦げた黒が混じる

という要素をノイズで表現している。

炎のゆらぎは「規則+不規則」の組み合わせ。
→ sin は規則、noise は不規則。
→ これを動かすと“炎が生まれる”。

GLSL と HLSL の差分はほぼゼロなので、
Three.js と Unity を横並びで理解できるのが最大の強み。

4. 水の透明度:depth でやる

水が “浅い部分は明るく、深い部分は暗く見える” のは
実際のゲームでもシェーダーでも、
「深度(depth)」を使って表現されている。

ここまで来ると 水らしさの本質 に触れる。

水面の波=sin
炎=noise
そして水の透明度=depth(奥行き)

この3つを押さえるだけで、水表現の土台が完成する。

Unity(URP):_CameraDepthTexture を使う

Unity URP では、シェーダーから深度テクスチャを読むことで
“水の下の地面との距離” を取れる。

これが透明度の変化につながる。

① Depth Texture を有効化する

URP Renderer の Depth Texture を ON にする。

② Depth の値を取得する

Fragment shader で _CameraDepthTexture を読む。

TEXTURE2D(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);

float4 frag(v2f i) : SV_Target
{
    float depth01 = SAMPLE_TEXTURE2D(
        _CameraDepthTexture,
        sampler_CameraDepthTexture,
        i.screenPos.xy / i.screenPos.w
    ).r;

    // 深度から線形距離を復元(URP必須)
    float linearDepth = LinearEyeDepth(depth01);

    // 浅い → 透明、深い → 不透明
    float alpha = saturate(1.0 - (linearDepth * 0.2));

    return float4(0.0, 0.5, 0.7, alpha);
}

解説

  • i.screenPos は頂点シェーダーで ComputeScreenPos して渡す
  • LinearEyeDepth() で本物の距離に変換
  • 距離が短い(浅い)ほど α は大きく(透明に近い)
  • 距離が長い(深い)ほど α は小さく(濃く)

こうして 「浅い水は透明で、深い水は濃い」 が作れる。

URP の水表現の本質部分。


Three.js:gl_FragCoord.z を使う

Three.js では ShaderMaterial({ depth: true }) の設定と、 gl_FragCoord.z を使って深度を取得する。

uniform sampler2D depthTexture;
uniform float cameraNear;
uniform float cameraFar;

float getLinearDepth(float depth)
{
    // 通常の線形化
    return cameraNear * cameraFar /
           (cameraFar - depth * (cameraFar - cameraNear));
}

void main() {
    // 画面の深度座標
    float depth = texture2D(depthTexture, gl_FragCoord.xy / resolution).r;

    float linearDepth = getLinearDepth(depth);

    // 浅い → 明るい、深い → 暗い
    float alpha = 1.0 - smoothstep(0.0, 20.0, linearDepth);

    gl_FragColor = vec4(0.0, 0.5, 0.7, alpha);
}

解説

  • depthTexture を THREE.WebGLRenderer で生成 (renderer.getDepthTexture() または depthTexture: new THREE.DepthTexture()
  • gl_FragCoord.xy で画面上の位置
  • gl_FragCoord.z や depthTexture の値は非線形(歪んでいる) → 線形化が必要
  • Unity と Three.js の計算は数学的に同じ

結論:水の透明感は「深度の差」で作る

水の透明度は “水面 → 地面の距離” を計算するだけ。

距離が短い(浅い) → 透明 距離が長い(深い) → 暗い・青い・濁る

Unity と Three.js の違いは 書き方だけ で、 仕組みは完全に同じ。

5. 発光(emission)と Fresnel

水や炎よりも“魔法っぽさ”“SFっぽさ”を一気に出せるのが 発光(emission) と Fresnel(フレネル効果)。

特に Fresnel は 「物体の縁だけが光る」 というリムライト表現で、ゲーム・VFX の必須テクニック。

基本はたったこれだけ:

法線ベクトルと、視線ベクトルの角度差を使う

角度差が大きい(= 物体の端 / 縁)ほど光る。 つまり、自然に“縁だけ光る” 演出が作れる。

Three.js と Unity の違いは“取得方法”だけで、 計算式は完全に同じ。


🔷 GLSL(Three.js)

Three.js では カメラ位置 と ワールド座標の法線 を使う。

varying vec3 vWorldNormal;
varying vec3 vWorldPosition;
uniform vec3 cameraPosition;

void main() {
    vec3 normal = normalize(vWorldNormal);

    // 視線ベクトル(カメラ → ピクセル)
    vec3 viewDir = normalize(cameraPosition - vWorldPosition);

    // Fresnel:法線と視線の角度差
    float fresnel = pow(1.0 - dot(normal, viewDir), 3.0);

    vec3 color = vec3(0.2, 0.6, 1.0) * fresnel; // 青く発光

    gl_FragColor = vec4(color, 1.0);
}

ポイント

  • cameraPosition - vWorldPosition が視線ベクトル
  • dot(normal, viewDir) が角度差(1 に近いほど正面)
  • 1 - dot(...) で “縁(端)ほど値が大きい”
  • pow(..., 3.0) で縁の発光を強調
  • Fresnel 色 × base color にすると綺麗な発光

Three.js では “世界座標の法線” を VS から渡す必要がある。


HLSL(Unity URP)

Unity は i.normalWS(ワールド空間の法線)と i.viewDir(視線方向)が標準で取得できる。

float4 frag(v2f i) : SV_Target
{
    float3 normal = normalize(i.normalWS);
    float3 viewDir = normalize(i.viewDir);

    // Fresnel
    float fresnel = pow(1.0 - dot(normal, viewDir), 3.0);

    // 発光色(シアン系)
    float3 emissive = float3(0.2, 0.6, 1.0) * fresnel;

    return float4(emissive, 1.0);
}

Unity のポイント

  • i.normalWS → 頂点シェーダーで UnityObjectToWorldNormal(v.normal)
  • i.viewDirGetWorldSpaceViewDir(i.worldPos)
  • Fresnel の式は GLSL と全く同じ
  • emission は “加算” っぽく見えるので SF 系と相性抜群

Fresnel を知ると一気に“中級”になる

Fresnel は実は:

  • ガラス
  • 水面の縁
  • シールド / バリア
  • 魔法のエフェクト
  • キャラの outline
  • ホログラム
  • エネルギーコアの光
  • 光るオーラ
  • 3D UI
  • ロボットの縁ライト
  • VFX(爆発・衝撃波)

など、あらゆる表現の背景にある数学。

Three.js と Unity で書き方がほぼ同じなので、 ここまで来ると HLSL/GLSL の壁が完全になくなる。

6. 応用サンプル(波 × 透明 × 反射の複合)

ここまでに学んだ

  • sin(波)
  • noise(炎)
  • depth(透明度)
  • Fresnel(縁の発光)

この 4 つを組み合わせると、いきなり “作品で使える品質” に跳ね上がる。
ゲームの水面や魔法表現は、ほとんどがこの組み合わせで作られている。

以下に、実際のゲームでよく使う 4 つの例を示す。


1. 水面(波 × 透明 × 反射)

使う要素:sin + depth + fresnel(少し)

  • sin で頂点を揺らす(水の動き)
  • depth で浅い部分を明るく、深い部分を暗く
  • fresnel で縁に少し発光(リアルな反射っぽさ)

Unity の場合、reflection probe を軽く足すだけで 一気に“実際のゲームの水”になる。

Three.js でも環境マップ(envMap)を加算すれば同じ。


2. ガラス(透明 × fresnel)

使う要素:depth + fresnel

  • depth → 背景との距離で透明度が変わる
  • fresnel → 薄いガラスの縁が光る(現実の屈折感)
    • environment map → 反射

ゲームの“近未来ガラス”や UI ホログラムはこの組み合わせで作られている。


3. 光るオブジェクト(emission × fresnel)

使う要素:emission + fresnel

  • emission → 内部発光
  • fresnel → 外縁が光る

これだけで、

  • エネルギーコア
  • 魔法の結晶
  • パワーアップアイテム
  • SF デバイス
  • 光る看板

などの“ゲームらしい光り方”が実現できる。

特に Fresnel は「縁だけ光る=立体感が増す」ので 初心者でも簡単に「プロっぽさ」を出せる。


4. 煙・火柱(noise × emission)

使う要素:noise + time + emission

  • noise のスクロールで揺らぎ
  • time を入れて上方向に伸びる
  • emission で明るさを足す

煙・火柱・魔法エフェクトの基礎は 「ノイズを動かす」だけ。

Three.js でも Unity でも GLSL/HLSL の違いを意識せずに書ける。


結論:

ここで紹介した 4つはすべて

sin / noise / depth / fresnel の組み合わせだけで成立している。

初心者が最短で“作品になる品質”へ行くには、 この 4 本柱を覚えるのが一番効率がいい。

ここまでマスターすると、 SimpleWater の中身も、Stylized Water の原理も、 ポストエフェクトの原理まで自然に読み解ける。

7. まとめ

シェーダーは一見すると難しく見えるが、
ゲームや 3D 表現で頻出する効果のほとんどは
たった 4 つの基本要素の組み合わせで作られている。

  • sin(波)
  • noise(炎・煙のゆらぎ)
  • depth(透明度・水の深さ)
  • fresnel(縁の発光・ガラスの輝き)

Three.js(GLSL)でも Unity(HLSL)でも、
根本的な考え方は完全に同じで、
違うのは“書き方”と“API”だけ。


この 4 つを理解するとできること

  • 水面(波 × depth × 反射)
  • 炎(noise × emission)
  • ガラス(fresnel × 透明)
  • 光るオブジェクト(fresnel × emission)
  • 煙・火柱(noise × time)
  • ホログラム / 魔法 / エネルギー体
  • リムライト(縁だけ光るキャラ表現)
  • Stylized Water(Unity/Three.jsの水表現の仕組み)
  • ポストエフェクトの基礎
  • VFX Graph / Shader Graph の理解も速くなる

つまり、 「テーマ別」で押さえると、いきなり作品レベルにジャンプできる。


そして何より、Three.js と Unity を“横で比較する”学習は強い

  • GLSL(Three.js)
  • HLSL(Unity)

両方で同じ効果を書く経験は、
シェーダー初心者にとって最強の成長ブーストになる。

あなたは既に Three.js / Unity を毎日触っているので、
まさに “最短で理解するための黄金コース” に乗っている。


次回(第6回)に向けて

次回は VR / WebXR / Unity XR における
「シェーダー特有の罠と最適化」を扱う。

  • ステレオレンダリング
  • マルチパスの注意
  • 透明シェーダーの落とし穴
  • VR の負荷最適化
  • WebXR と Unity の描画の違い
  • 反射・屈折・透明の“XR特有の問題”

ここまで来ると、
あなたの得意分野(WebXR × Unity)と完全に融合する。