プロシージャル

テクスチャを使わず関数だけで模様を作るのがプロシージャル。 サイズ無限・ロード不要・パラメータで自由に変えられる。 SDF・ノイズ・パターンの3つを覚えると、創作の幅が一気に広がる。

SDF(Signed Distance Field)

各点から「形までの最短距離」を返す関数。負の値ならその形の内側、正なら外側。 境界は 0

基本図形の SDF

// 円
float sdCircle(vec2 p, float r) {
  return length(p) - r;
}

// 矩形
float sdBox(vec2 p, vec2 b) {
  vec2 d = abs(p) - b;
  return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}

// 角丸矩形
float sdRoundedBox(vec2 p, vec2 b, float r) {
  return sdBox(p, b - r) - r;
}

// 線分
float sdSegment(vec2 p, vec2 a, vec2 b) {
  vec2 pa = p - a, ba = b - a;
  float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
  return length(pa - ba * h);
}

距離から色を作る

SDF から塗り分け・縁取り・グラデーションが作れる:

float d = sdCircle(vUv - 0.5, 0.3);

// 塗り
float fill = smoothstep(0.005, -0.005, d);

// 縁取り
float edge = abs(d) < 0.01 ? 1.0 : 0.0;

// 距離をそのまま色化
vec3 col = vec3(0.5 + 0.5 * sin(d * 30.0));

SDF の組み合わせ(ブール演算)

float opUnion(float a, float b)        { return min(a, b); }
float opSubtraction(float a, float b)  { return max(-a, b); }
float opIntersection(float a, float b) { return max(a, b); }

// 滑らかな union(融合)
float opSmoothUnion(float a, float b, float k) {
  float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
  return mix(b, a, h) - k * h * (1.0 - h);
}

SDF をアニメーションする

vec2 p = vUv - 0.5;

float d1 = sdCircle(p - vec2(sin(uTime) * 0.2, 0.0), 0.2);
float d2 = sdCircle(p + vec2(sin(uTime) * 0.2, 0.0), 0.2);
float d  = opSmoothUnion(d1, d2, 0.05);   // メタボール

float c = smoothstep(0.005, -0.005, d);
outColor = vec4(vec3(c), 1.0);

3D SDF(Raymarching)

2D だけでなく 3D の SDF も同じ概念で書ける。3D SDF をraymarching で描画すると、 ジオメトリ無しで立体を表現できる。詳しくは Inigo Quilez の distfunctions

ノイズ

ランダムなのに「滑らか」な値を作る関数。雲・水面・地形・ヒビ等の自然現象に欠かせない。

1. ハッシュ(疑似ランダム)

ノイズの土台。同じ入力に対して毎回同じ値を返す決定論的な「乱数」。

// 1D
float hash11(float p) {
  p = fract(p * 0.1031);
  p *= p + 33.33;
  p *= p + p;
  return fract(p);
}

// 2D → 1D
float hash21(vec2 p) {
  vec3 p3 = fract(vec3(p.xyx) * 0.1031);
  p3 += dot(p3, p3.yzx + 33.33);
  return fract((p3.x + p3.y) * p3.z);
}

// 2D → 2D
vec2 hash22(vec2 p) {
  vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));
  p3 += dot(p3, p3.yzx + 33.33);
  return fract((p3.xx + p3.yz) * p3.zy);
}

sin ベースの古典的なハッシュもよく見るが、精度の問題があるので整数演算ベースの上記のような実装が安全。

2. Value ノイズ

グリッド上のランダム値を滑らかに補間。最も基本的なノイズ。

float valueNoise(vec2 p) {
  vec2 i = floor(p);
  vec2 f = fract(p);

  // 4 隅の値
  float a = hash21(i);
  float b = hash21(i + vec2(1.0, 0.0));
  float c = hash21(i + vec2(0.0, 1.0));
  float d = hash21(i + vec2(1.0, 1.0));

  // smoothstep で補間
  vec2 u = f * f * (3.0 - 2.0 * f);
  return mix(a, b, u.x) +
         (c - a) * u.y * (1.0 - u.x) +
         (d - b) * u.x * u.y;
}

3. Gradient ノイズ(Perlin / Simplex 系)

グリッドにランダムな勾配ベクトルを持たせて、内積で値を決める。 Value ノイズより立体的な見た目になる。実装が長いので既存の関数をコピペする人が多い。

Ashima/Stefan Gustavson の snoise が定番。検索で出る。実装の中身を理解する必要は最初は無い。

4. Voronoi(セル)ノイズ

グリッド内のランダム点までの最短距離細胞・割れ・タイル模様に。

float voronoi(vec2 p) {
  vec2 i = floor(p);
  vec2 f = fract(p);

  float minDist = 1.0;
  for (int y = -1; y <= 1; y++) {
    for (int x = -1; x <= 1; x++) {
      vec2 neighbor = vec2(float(x), float(y));
      vec2 point = hash22(i + neighbor);
      vec2 diff = neighbor + point - f;
      float dist = length(diff);
      minDist = min(minDist, dist);
    }
  }
  return minDist;
}

FBM(Fractal Brownian Motion)

ノイズを周波数を上げながら重ねると、自然な複雑さが出る。

float fbm(vec2 p) {
  float v = 0.0;
  float a = 0.5;
  for (int i = 0; i < 5; i++) {
    v += a * valueNoise(p);
    p *= 2.0;
    a *= 0.5;
  }
  return v;
}

// 雲の応用
vec2 q = vUv * 4.0;
q.x += uTime * 0.1;
float n = fbm(q);
outColor = vec4(vec3(n), 1.0);

ドメインワーピング(Inigo Quilez 流)

座標をノイズで歪ませてからもう一度ノイズを通す。劇的に複雑な模様が作れる:

float warpedFbm(vec2 p) {
  vec2 q = vec2(fbm(p), fbm(p + vec2(1.0, 0.0)));
  vec2 r = vec2(fbm(p + 4.0 * q + vec2(uTime * 0.3, 0.0)),
                fbm(p + 4.0 * q + vec2(0.0, uTime * 0.3)));
  return fbm(p + 4.0 * r);
}

パターン集

波紋

vec2 p = vUv - 0.5;
float r = length(p);
float wave = sin(r * 30.0 - uTime * 4.0) * 0.5 + 0.5;
outColor = vec4(vec3(wave), 1.0);

放射状の縞

vec2 p = vUv - 0.5;
float a = atan(p.y, p.x);
float c = step(0.5, fract(a / 6.28 * 12.0));
outColor = vec4(vec3(c), 1.0);

ハニカム / 六角形タイル

vec2 hex(vec2 p) {
  vec2 q = vec2(p.x * 2.0 * 0.5773503, p.y + p.x * 0.5773503);
  vec2 i = floor(q);
  vec2 f = fract(q);
  float v = mod(i.x + i.y, 3.0);
  return vec2(v, length(f - 0.5));
}

レンガ

vec2 p = vUv * vec2(8.0, 16.0);
p.x += step(1.0, mod(p.y, 2.0)) * 0.5;   // 行ごとに半分ずらす
vec2 ip = floor(p);
vec2 fp = fract(p);

float mortar = step(0.05, fp.x) * step(0.05, fp.y) *
               step(fp.x, 0.95) * step(fp.y, 0.95);
outColor = vec4(vec3(mortar), 1.0);

水面(時間で変化するノイズ)

float water(vec2 p, float t) {
  return sin(p.x * 4.0 + t) * 0.5 +
         sin(p.y * 6.0 + t * 1.3) * 0.5 +
         sin((p.x + p.y) * 5.0 + t * 0.7) * 0.5;
}

シマシマアニメ(流れる帯)

vec2 p = vUv;
p.x += uTime * 0.2;
float stripe = step(0.5, fract(p.x * 10.0));
outColor = vec4(vec3(stripe), 1.0);

炎風

vec2 p = vUv;
p.y -= uTime * 0.3;
float n = fbm(p * 3.0);
float flame = (1.0 - vUv.y) * n;
flame = smoothstep(0.3, 0.6, flame);

vec3 col = mix(vec3(1.0, 0.2, 0.0), vec3(1.0, 0.9, 0.5), flame);
outColor = vec4(col * flame, 1.0);

カラーグラデーション関数

2 色補間

vec3 col = mix(colorA, colorB, t);

n 色の段階補間

vec3 palette(float t) {
  // Inigo Quilez の cosine palette
  vec3 a = vec3(0.5, 0.5, 0.5);
  vec3 b = vec3(0.5, 0.5, 0.5);
  vec3 c = vec3(1.0, 1.0, 1.0);
  vec3 d = vec3(0.0, 0.33, 0.67);
  return a + b * cos(6.28318 * (c * t + d));
}

a + b * cos(2π * (c * t + d)) という4つのベクトルだけで多彩なカラーパレットを作れる魔法の式。 Inigo Quilez の 「palettes」 記事に詳しい。

歪みエフェクト

波打ち

vec2 uv = vUv;
uv.x += sin(uv.y * 30.0 + uTime * 2.0) * 0.02;
outColor = texture(uTex, uv);

RGB シフト(色収差)

float r = texture(uTex, vUv + vec2(0.005, 0.0)).r;
float g = texture(uTex, vUv).g;
float b = texture(uTex, vUv - vec2(0.005, 0.0)).b;
outColor = vec4(r, g, b, 1.0);

ピクセル化

float size = 64.0;
vec2 uv = floor(vUv * size) / size;
outColor = texture(uTex, uv);

水面屈折

vec2 noise = vec2(
  sin(vUv.x * 10.0 + uTime),
  cos(vUv.y * 10.0 + uTime)
) * 0.01;
outColor = texture(uTex, vUv + noise);

SDF をアウトライン化

float d = sdCircle(vUv - 0.5, 0.3);

float thickness = 0.005;
float outline = smoothstep(thickness, 0.0, abs(d));
outColor = vec4(vec3(outline), 1.0);

fwidth でアンチエイリアス

fwidth を使うとピクセル単位で一定の太さのラインや境界が描ける。 スケールに依存しない rendering の鉄板テク:

float aaCircle(vec2 p, float r) {
  float d = length(p) - r;
  float w = fwidth(d) * 1.5;
  return smoothstep(w, -w, d);
}

float c = aaCircle(vUv - 0.5, 0.3);

raymarching の超概要

3D 空間に SDF を置いて、カメラからレイを飛ばして少しずつ進む方法。 各ステップで「最短距離」分進めるので、距離関数を書くだけで 3D 形状が描ける。

float scene(vec3 p) {
  return length(p) - 1.0;   // 単位球
}

float raymarch(vec3 ro, vec3 rd) {
  float t = 0.0;
  for (int i = 0; i < 64; i++) {
    vec3 p = ro + rd * t;
    float d = scene(p);
    if (d < 0.001) return t;
    if (t > 100.0) break;
    t += d;
  }
  return -1.0;
}

vec3 normal(vec3 p) {
  vec2 e = vec2(0.001, 0.0);
  return normalize(vec3(
    scene(p + e.xyy) - scene(p - e.xyy),
    scene(p + e.yxy) - scene(p - e.yxy),
    scene(p + e.yyx) - scene(p - e.yyx)
  ));
}

本格的な raymarching は奥が深いので、Shadertoy の作品を読むのが最短。

学習リソース

- The Book of Shaders: 章ごとに pattern / random / noise / fbm を学べる
- Inigo Quilez articles: SDF / raymarching / noise / palette の決定版
- Shadertoy: 作品を覗いてコピーして手を動かす