プロシージャル
テクスチャを使わず関数だけで模様を作るのがプロシージャル。 サイズ無限・ロード不要・パラメータで自由に変えられる。 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: 作品を覗いてコピーして手を動かす