フラグメントシェーダ(Fragment Shader / Pixel Shader)

ピクセル(fragment)ごとに 1 回呼ばれ、そのピクセルの最終的な色を計算する。 画面の見た目を決めるのは大半このシェーダ。創造性の発揮場所。

役割

  1. 頂点シェーダから補間された varying(in)を受け取る
  2. uniform(時間・テクスチャ・色等)を読む
  3. 計算して色(RGBA)を出す

最小コード

fragment.glsl(GLSL ES 3.00)
#version 300 es
precision highp float;

in vec2 vUv;
out vec4 outColor;

void main() {
  outColor = vec4(vUv, 0.0, 1.0);   // 左下から赤緑グラデーション
}
Three.js(自動 precision、varying でも可)
varying vec2 vUv;
void main() {
  gl_FragColor = vec4(vUv, 0.0, 1.0);
}

出力のしかた

使える組み込み変数

変数意味
gl_FragCoord画面ピクセル座標(左下原点、xy はピクセル単位)
gl_PointCoordPoints 描画時の点内座標(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 はパフォーマンスを落とす

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 ベクトルの基本演算

フルスクリーンシェーダ

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);
}

よくある失敗