フラグメントシェーダ(Fragment Shader / Pixel Shader)
ピクセル(fragment)ごとに 1 回呼ばれ、そのピクセルの最終的な色を計算する。 画面の見た目を決めるのは大半このシェーダ。創造性の発揮場所。
役割
- 頂点シェーダから補間された
varying(in)を受け取る - uniform(時間・テクスチャ・色等)を読む
- 計算して色(RGBA)を出す
最小コード
#version 300 es
precision highp float;
in vec2 vUv;
out vec4 outColor;
void main() {
outColor = vec4(vUv, 0.0, 1.0); // 左下から赤緑グラデーション
}
varying vec2 vUv;
void main() {
gl_FragColor = vec4(vUv, 0.0, 1.0);
}
出力のしかた
- GLSL ES 3.00:
out vec4 outColor;を宣言して書き込む - GLSL ES 1.00:
gl_FragColorという組み込み変数に書く - RGBA は0..1 の範囲(HDR 用には 1 を超えられる)
- α=1.0 で不透明、0.0 で完全透明
使える組み込み変数
| 変数 | 意味 |
|---|---|
gl_FragCoord | 画面ピクセル座標(左下原点、xy はピクセル単位) |
gl_PointCoord | Points 描画時の点内座標(0..1) |
gl_FrontFacing | 表面なら true |
gl_FragDepth | 書き込みで深度を上書き(低レベル) |
discard | そのピクセルの描画を放棄 |
gl_FragCoord の使い方
画面ピクセル単位の座標。例えば 1920×1080 の canvas で右上のピクセルなら (1919.5, 1079.5)。
uniform vec2 uResolution; // canvas サイズを JS から渡す
void main() {
// 画面の中で 0..1 の座標を作る
vec2 uv = gl_FragCoord.xy / uResolution.xy;
outColor = vec4(uv, 0.0, 1.0);
}
vUv(頂点シェーダから来る uv)と gl_FragCoord は別物。
前者はメッシュの UV(テクスチャ座標)、後者は画面ピクセル位置。
色を出す典型パターン
単色
outColor = vec4(0.5, 0.7, 1.0, 1.0); // 水色
UV を色に
in vec2 vUv;
void main() {
outColor = vec4(vUv, 0.0, 1.0);
}
2色のグラデーション
vec3 a = vec3(0.0, 0.5, 1.0);
vec3 b = vec3(1.0, 0.0, 0.5);
outColor = vec4(mix(a, b, vUv.y), 1.0);
3色以上のグラデーション
vec3 col;
if (vUv.y < 0.5) {
col = mix(vec3(1.0,0.0,0.0), vec3(0.0,1.0,0.0), vUv.y * 2.0);
} else {
col = mix(vec3(0.0,1.0,0.0), vec3(0.0,0.0,1.0), (vUv.y - 0.5) * 2.0);
}
outColor = vec4(col, 1.0);
放射グラデーション(中心から)
vec2 p = vUv - 0.5;
float r = length(p);
vec3 col = mix(vec3(1.0), vec3(0.0), r * 2.0);
outColor = vec4(col, 1.0);
図形を描く
円
vec2 p = vUv - 0.5;
float d = length(p); // 中心からの距離
float c = step(d, 0.3); // 0.3 以下なら 1、それ以外 0
// アンチエイリアスありの円
float a = smoothstep(0.31, 0.29, d);
outColor = vec4(vec3(a), 1.0);
矩形
vec2 p = abs(vUv - 0.5);
vec2 s = step(p, vec2(0.3, 0.2));
float box = s.x * s.y;
outColor = vec4(vec3(box), 1.0);
チェッカー
vec2 g = floor(vUv * 8.0);
float c = mod(g.x + g.y, 2.0);
outColor = vec4(vec3(c), 1.0);
縞模様
float s = step(0.5, fract(vUv.x * 10.0));
outColor = vec4(vec3(s), 1.0);
discard でそのピクセルを描かない
透明(α=0)と違い、discard はそもそもピクセルを書かない。深度バッファにも書かない。
葉っぱの抜き、α テスト、円形のスタンプ等に。
vec2 p = vUv - 0.5;
if (length(p) > 0.5) discard;
outColor = vec4(1.0); // 円の中だけ白
discard が無いと GPU は早期に深度書き込みなどを最適化できる。大量に discard を書くと遅くなる。
単純な α 合成で済むなら transparent + α の方が速い。
varying(in)から色を作る
頂点シェーダから渡された値を使って、面の状態を見ながら色を決める。
in vec3 vNormal;
in vec3 vViewDir;
void main() {
vec3 N = normalize(vNormal);
vec3 V = normalize(vViewDir);
// フレネル: 真横を向いた面ほど 1 に近づく
float fresnel = pow(1.0 - max(dot(N, V), 0.0), 3.0);
vec3 base = vec3(0.1, 0.3, 0.6);
vec3 rim = vec3(1.0, 0.8, 0.3);
outColor = vec4(mix(base, rim, fresnel), 1.0);
}
テクスチャから色を取る
詳しくは テクスチャ。最短:
uniform sampler2D uTex;
in vec2 vUv;
out vec4 outColor;
void main() {
outColor = texture(uTex, vUv); // ES 1.00 では texture2D
}
透明・ブレンド
Three.js のマテリアル側で transparent: true を設定する必要がある(α が効くようにブレンドが入る)。
// fragment 側
outColor = vec4(1.0, 0.5, 0.2, 0.6); // 60% 不透明
時間で動かす
uniform float uTime;
in vec2 vUv;
void main() {
float r = sin(uTime + vUv.x * 6.28) * 0.5 + 0.5;
outColor = vec4(r, 0.5, 1.0, 1.0);
}
JS 側(Three.js)で uTime を毎フレーム更新する:
// R3F
useFrame(({ clock }) => {
material.uniforms.uTime.value = clock.getElapsedTime()
})
ピクセルの座標を絶対値で扱う
vUv はメッシュの UV なので歪む。
画面上で正方形の演出をしたい時は gl_FragCoord + resolution を使う。
uniform vec2 uResolution;
void main() {
vec2 uv = gl_FragCoord.xy / uResolution.xy;
uv -= 0.5;
uv.x *= uResolution.x / uResolution.y; // アスペクト補正
float d = length(uv);
float c = smoothstep(0.31, 0.29, d);
outColor = vec4(vec3(c), 1.0);
}
2D ベクトルの基本演算
length(v)— 長さ(中心からの距離)distance(a, b)— 2点間距離dot(a, b)— 内積(同方向なら 1、直交 0、逆 -1)normalize(v)— 長さを 1 にfract(v)— 小数部のみfloor(v)— 切り捨てmod(a, b)— 剰余abs(v)— 絶対値step(edge, x)— x >= edge なら 1、それ以外 0smoothstep(e0, e1, x)— e0..e1 で滑らかに 0..1mix(a, b, t)— 線形補間
フルスクリーンシェーダ
Mesh ではなく画面いっぱいの板に fragment shader を貼って、procedural な背景や post-effect を作る。 詳しくは R3F のシェーダページ。
典型パターン集
HSV → RGB
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0/3.0, 1.0/3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
2D 回転行列
mat2 rot2(float a) {
float s = sin(a), c = cos(a);
return mat2(c, -s, s, c);
}
// 使い方
vec2 p = (vUv - 0.5) * rot2(uTime);
ガンマ補正(リニア → sRGB を自前で)
Three.js では r152+ で自動で sRGB 変換されるので普通は不要だが、 手動シェーダや FBO 経由のときに必要になることがある。
vec3 toSrgb(vec3 linear) {
return pow(linear, vec3(1.0 / 2.2));
}
fragment 出力を複数に(MRT / Multiple Render Targets)
WebGL2 では out を複数宣言して、複数の RenderTarget に書き分けできる。
layout(location = 0) out vec4 colorOut;
layout(location = 1) out vec4 normalOut;
layout(location = 2) out vec4 depthOut;
void main() {
colorOut = ...;
normalOut = vec4(N * 0.5 + 0.5, 1.0);
depthOut = vec4(vec3(gl_FragCoord.z), 1.0);
}
よくある失敗
- 真っ黒になる —
outColorを書き忘れた、またはvec4(0,0,0,1)を出してる - varying 名前ミスマッチ — vertex の
outと fragment のinで名前が違う - α が無視される — マテリアルに
transparent: true必要 vUvがずれる — geometry の UV を確認。PlaneGeometry等は左下原点- アスペクト比で歪む —
gl_FragCoord使う時は resolution で補正 - カラーが薄い・濃い — sRGB / リニアの変換問題(Three.js テクスチャ参照)