はじめに
第5回のテーマ
- 水面(SimpleWater)
- 炎(noise)
- 発光(emission + fresnel)
- 深度を使った透明度
- 法線で反射を調整
前回の記事:
[Shader 入門 #04] Three.js vs Unity:シェーダー構造を横並び徹底比較(GLSL / HLSL 二刀流の脳)
GLSL と HLSL の違いは文法より“受け渡しの作法”。Three.js と Unity のシェーダー構造を横並びで比較し、attribute→appdata、varying→v2f、time/UV/normal、model/view/projection …
https://humanxai.info/posts/shader-intro-04-threejs-unity-structure-compare-glsl-hlsl/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(不規則) を使う。
基本ステップは共通:
- ノイズ(Perlin / Simplex)を取得
- 時間 time を足して“上方向に流れる”動き
- ノイズ値で色(yellow → red → black)を補間
- 上に行くほど透明にして炎っぽさを出す
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.viewDir→GetWorldSpaceViewDir(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)と完全に融合する。
💬 コメント