8. 乱数とノイズ

此れまで描いた物は全て完全に規則的でした。
正確な円、真っ直ぐなレンガ、同じ目盛りが並んだ時計。
自然はそうではありません。
雲、岩、木、炎、水、地形、錆には構造がありますが、全く同じに見える場所は二つとありません。
此の章では、シェーダーで其の様な「制御された不規則さ」を作る方法を学びます。
先ず乱数、次にノイズ、つまり滑らかに変化する乱数です。

GPUに何故 rand() が全く無いのかから始め、整数のビット演算でちゃんとしたハッシュ関数を作り、其れからバリューノイズ、グラディエントノイズ(パーリンノイズの仲間)、フラクタルノイズ(fBm)、ドメインワーピング、セルラー(ボロノイ)ノイズを作ります。
最後は、どのゲームでも見るエフェクト(ディゾルブと炎)と、山・雲・星が全てノイズで出来た夕焼けの風景です。

此れまで通り、変更するのは shader/playground.glsl だけです。
第6章と第7章の関数(sdCircle、sdRoundBox、fill、paint、rot 等)はファイルの先頭にそのまま残っています。

8.1 何故rand()が無いのか

Cでは rand() を呼ぶと毎回違う数が返って来ます。
其れは rand() が隠れた状態(シード)を持っていて、呼ぶ度に其れが変わるからです。
フラグメントシェーダーには其れが出来ません。
何十万ものフラグメントシェーダーが決まった順番無しに同時に実行され、其々が書き換える変数を共有する事は出来ません。
続きを始める「前回の呼び出し」が無いのです。
仮にあったとしても、GPUが偶々ピクセルを処理した順番によって、毎フレーム絵が変わってしまいます。

其の為、シェーダーには別の物が必要です。
入力(ピクセルの位置、マスの番号、時間)を受け取り、ランダムに見えるけれど同じ入力なら常に同じ数を返す関数です。
此の様な関数をハッシュ関数と呼びます。
同じ入力なら同じ出力、少しだけ違う入力なら全く違う出力です。

h:入力↦[0,1),𝐚=𝐛 なら h(𝐚)=h(𝐛)

此れは制限ではなく、利点です。
マスの「乱数」がマスの番号だけで決まるので、マスの中の全てのピクセルが他と話さずに同じ値を得られ、絵は毎フレーム同じで、必要な時には何時でも隣のマスの乱数を求められます(ノイズは常に其れをします)。

8.2 古典的なsinのハッシュ

ネット上のシェーダーのコードを読むと、此の行を至る所で見掛けます:

// 昔からよく使われる sin のハッシュ
float hashSin(vec2 p) {
  return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}

どの様に動くのでしょうか?
dot(p, vec2(12.9898, 78.233)) は2Dの入力を一つの数に変換します。
変な係数を使っているのは、隣の入力でかなり違う値になる様にする為です。
其の sin は−1〜1の数です。
43758.5453を掛けて大きくし、fract で小数点以下の桁だけを残します。
其の桁は入力が少し変わると激しく変わるので、結果はランダムに見えます。

短くて大抵は動きますが、深刻な問題が二つあります。

  1. GPUが sin をどれ位正確に計算するかに依存します。 fract が残すのはサイン値の5桁目から10桁目です。32ビットの float の有効桁数は全部で約7桁しかなく、GPUは特に大きな引数で sin を其れより不正確に計算する事が許されています。或るGPUではランダムに見え、別のGPUでは縞が出て、スマートフォンでは酷い事になるかも知れません。
  2. 原点から遠いと壊れます。 入力が約107だと、dot は約1.3×108になります。其の大きさでは float は8の倍数しか表せないので、sin の引数が丸められ、隣のマス同士で関係のある結果になります。

二つ目の問題を見てみましょう。
此のシェーダーは4×4ピクセルのマス毎にランダムな灰色を付けます。
左半分では原点の近くのマスの番号を、右半分では1000万ずらしたマスの番号を使います:

void main() {
  vec2 cell = floor(gl_FragCoord.xy / 4.0);       // 4×4ピクセルのマス
  float n;
  if (gl_FragCoord.x < uResolution.x * 0.5) {
    n = hashSin(cell);                            // 左:原点の近く
  } else {
    n = hashSin(cell + vec2(10000000.0, 0.0));    // 右:原点から遠く(1000万)
  }
  out_color = vec4(vec3(n), 1.0);
}

上:sinのハッシュ。左はランダムだが右は規則的な格子模様。下:整数のハッシュ。両側ともランダム

画像の上半分がsinのハッシュです。
右側に点と縞の規則的な模様が現れています。
下半分は次の節の整数のハッシュで、入力は全く同じですが、両側ともランダムです。
(スクリーンショットはCPU上で動くMesaのソフトウェアレンダラーで作ったので、sin が正確で、此処まで遠くでやっと壊れます。本物のGPUでは、sinのハッシュはずっと小さな数で失敗する事があります。)
1000万は大きく聞こえますが、ミリ秒単位の uTime なら約2.8時間、センチメートル単位なら大きめのオープンワールド程度です。

8.3 整数のハッシュ

GLSL 1.30(OpenGL 3.0)以降、シェーダーには本物の32ビット整数 int と uint、及びCと同じビット演算があります。
整数の計算は正確で、どのGPUでも同じ結果になるので、ハッシュには最適な道具です。
考え方はCのライブラリの乱数生成器と同じです。
全ての入力ビットが全ての出力ビットに影響する様になるまで、シフト、XOR、大きな奇数との掛け算でビットを掻き混ぜます。

此れはChris Wellonsの「lowbias32」というハッシュで、最良の定数をコンピューターで探索して見つけた物です:

// 整数ハッシュ(Chris Wellonsのlowbias32):32ビットをよく掻き混ぜる
uint hashU(uint x) {
  x ^= x >> 16u;
  x *= 0x7feb352du;
  x ^= x >> 15u;
  x *= 0x846ca68bu;
  x ^= x >> 16u;
  return x;
}

此の数行の新しいGLSLの機能:

  • uint は符号無しの32ビット整数です。uint のリテラルには u の接尾辞が必要です(16u、0x7feb352du)。付けないと int になります。デスクトップのGLSL 4.00以降は int を uint に自動で変換しますが(第2章)、GLSL 1.30〜3.30とGLSL ES 3.00は一つの演算で int と uint を決して混ぜないので、u を付けておけばハッシュが何処でも動きます。
  • x >> 16u はビットを右に16桁ずらします。上半分が下半分に移ります。^ はビット毎のXORなので、x ^= x >> 16u は上のビットを下のビットに混ぜます。
  • x *= 0x7feb352du は大きな奇数を掛けます。結果は32ビットに収まりませんが、Cの unsigned と同じく、はみ出たビットは単に捨てられます。結果はx⋅mを232で割った余りです。掛け算は情報を下のビットから上のビットへ移し、次のシフトが其れを下に戻します。

5つのステップの後では、入力の1ビットを変えると出力のビットの約半分が変わります。
ビットにとって「ランダムに見える」とは其ういう事です。

2Dのマスに使うにはあと2つのステップが必要です。
マスのfloatの座標を uint に変換する事と、32ビットの結果を0〜1のfloatに変換する事です。

// 格子点(整数の座標)から0〜1の乱数を1つ
float hash21(vec2 cell) {
  uvec2 q = uvec2(ivec2(cell));   // 負の数もビットのまま変換される
  return float(hashU(q.x + hashU(q.y)) >> 8u) / 16777216.0;
}

// 格子点から0〜1の乱数を2つ
vec2 hash22(vec2 cell) {
  uvec2 q = uvec2(ivec2(cell));
  uint h = hashU(q.x + hashU(q.y));
  return vec2(float(h >> 8u), float(hashU(h) >> 8u)) / 16777216.0;
}

  • ivec2(cell) はfloatのベクトルを整数に変換します(小数部分は捨てられますが、floor からのマスの番号では0です)。uvec2(...) は其れを符号無しとして解釈し直します。負の数ではGLSLはビットの並びを保つので、−1は4294967295になり、全てのマスが違う数を得ます。
  • hashU(q.x + hashU(q.y)) は先にyをハッシュし、xを足してからもう一度ハッシュします。単にxとyを足すだけでは、マス(1, 2)と(2, 1)が同じ値になってしまいます。
  • float の精度は24ビットです。>> 8u でハッシュの上位24ビットを残し、224=16777216で割ると0から1の少し手前までの数になります。全ての値が正確に表せるので、結果は均等に散らばります。
  • hash22 は結果をもう一度ハッシュして、独立した2つ目の数を得ます。

Mark JarzynskiとMarc Olanoは「Hash Functions for GPU Rendering」(2020年)で多くのGPU用ハッシュ関数を比較しました。
此の様な整数のハッシュは、sinのハッシュと同程度のコストでずっと良い乱数になりました。

uint とビット演算子にはGLSL 1.30以降が必要です。デスクトップのOpenGL 3.0以降(macOSの4.1を含む)とOpenGL ES 3.0 / WebGL 2にはありますが、OpenGL ES 2.0とWebGL 1にはありません(其の int は本物の32ビット整数ですらないかも知れません)。其の為、其方ではsinのハッシュが広まりました。

ホワイトノイズは、上の画像の下半分の様に、ピクセル(又はマス)毎のハッシュに過ぎません。
古いテレビの様にチラつかせるには、入力にフレーム番号を足します:hash21(cell + floor(uTime * 30.0) * vec2(37.0, 17.0))。
ホワイトノイズは粒状感やディザリングに便利ですが、ピクセルからピクセルへ完全に変わります。
雲や地形にはもっと滑らかな物が必要です。

8.4 バリューノイズ

一番簡単な滑らかなノイズです。
全ての格子点(座標が整数の全ての点)にランダムな値を置き、其の間を混ぜます。

8.4.1 1Dでの考え方

1次元では、全ての整数iで乱数hiを取り、iとi+1の間では小数部分t=fract(x)を使って mix でhiからhi+1へ混ぜます:

n(x)=mix(hi,hi+1,u(t)),i=⌊x⌋,t=x−i

u(t)=t(線型補間)なら、整数毎に鋭い角のある直線になります(画像の赤)。
滑らかな曲線u(t)を使うと角が消えます(黄色):

白い点は整数0〜8での乱数。赤い線は其れを直線で結び、黄色い曲線は同じ点を滑らかに通る

8.4.2 曲線を選ぶ

滑らかな曲線は既に一つ知っています。
第6章のsmoothstepのt2(3−2t)です。
傾きが両端で0なので、角無く繋がります。
然し、其の曲率(2階微分、6−12t)はt=0で6、t=1で−6なので、整数毎に跳びます。
ノイズ其の物では見えませんが、ノイズを高さマップとして使い、其処からライティングを計算すると(第12章)、折れ目として現れます。
ライティングは傾きに依存し、其の傾きの変化が跳ぶからです。

Ken Perlinの改良版ノイズ(2002年)は、1階と2階の微分が両端で0になる5次の曲線を使います:

u(t)=6t5−15t4+10t3

0から1までの3つの曲線:直線t、3次のt²(3 − 2t)、より急な5次の6t⁵ − 15t⁴ + 10t³

両端を確かめましょう。
u(0)=0、u(1)=6−15+10=1です。
微分はu′(t)=30t4−60t3+30t2=30t2(t−1)2で、t=0とt=1で0です。
コードでは、掛け算が少なくて済む様に書きます(ホーナー法):

// 5次の補間曲線 6t^5 - 15t^4 + 10t^3(両端で1階と2階の微分が0)
vec2 quintic(vec2 t) {
  return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}

8.4.3 2Dのバリューノイズ

2Dでは、点は4つの角を持つ格子のマスの中にあります。
左下・右下・左上・右上の角の乱数a,b,c,dを取り、下の辺と上の辺でxに沿って混ぜ、其の2つの結果をyに沿って混ぜます。
此れを双線型補間と呼びます(直線の代わりに曲線を使っていますが):

n(𝐩)=mix(mix(a,b,ux),mix(c,d,ux),uy)

// バリューノイズ:格子点の乱数を滑らかに補間(0〜1)
float valueNoise(vec2 p) {
  vec2 i = floor(p);
  vec2 f = fract(p);
  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));
  vec2 u = quintic(f);
  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

角の値がa=0.2、b=0.8、c=0.4、d=0.6で、マスの中の点が𝐟=(0.25,0.5)の計算例:

ux=6(0.25)5−15(0.25)4+10(0.25)3=0.00586−0.05859+0.15625=0.10352uy=6(0.5)5−15(0.5)4+10(0.5)3=0.1875−0.9375+1.25=0.5mix(0.2,0.8,0.10352)=0.2+0.6×0.10352=0.26211mix(0.4,0.6,0.10352)=0.4+0.2×0.10352=0.42070n=0.26211+(0.42070−0.26211)×0.5=0.34141

曲線を比べる為に、同じ乱数の格子(1単位に4マス)を4通りに表示してみます:

// 補間の仕方だけを変えたバリューノイズ
float valueNoiseWith(vec2 p, int mode) {
  vec2 i = floor(p);
  vec2 f = fract(p);
  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));
  vec2 u = f;                                  // 0:線型補間
  if (mode == 1) u = f * f * (3.0 - 2.0 * f);  // 1:smoothstepと同じ3次曲線
  if (mode == 2) u = quintic(f);               // 2:5次曲線
  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

void main() {
  vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
  vec2 q = p * 4.0;

  // 左から:マスの乱数そのまま、線型、3次、5次
  float x = gl_FragCoord.x / uResolution.x;
  float n;
  if (x < 0.25)      n = hash21(floor(q));
  else if (x < 0.5)  n = valueNoiseWith(q, 0);
  else if (x < 0.75) n = valueNoiseWith(q, 1);
  else               n = valueNoiseWith(q, 2);

  vec3 color = vec3(n);
  color *= smoothstep(0.0, 0.004, abs(fract(x * 4.0 + 0.5) - 0.5));  // 区切りの線
  out_color = vec4(color, 1.0);
}

4本の帯:ランダムな灰色の正方形のブロック、星形の模様が出る線型補間、より滑らかな3次、格子点の周りに平らな所がある5次

線型補間では、格子の線に沿って明るい「星」と暗い「星」が見えます。
曲線で其れは消えますが、格子はまだ見えます。
バリューノイズは山と谷が丁度格子点にあり、其の周りの平らな所で正方形が見えてしまうのです。
グラディエントノイズが其れを解決します。

8.5 グラディエントノイズ

Ken Perlinのアイデア(1985年。後にアカデミー科学技術賞を受賞した研究)は、格子点にランダムな値ではなく、ランダムな傾きを置く事でした。

8.5.1 1Dでの考え方

全ての整数iでノイズは0になり、其の点からランダムな傾きgiで出て行きます。
点iの近くではノイズは直線gi(x−i)の様に振る舞い、i+1の近くではgi+1(x−i−1)の様に振る舞います。
其の間では、此れらの直線を5次の曲線で混ぜます:

n(x)=mix(git,gi+1(t−1),u(t)),t=x−i

青い線分は各整数でのランダムな傾き。黄色いノイズの曲線は全ての整数で丁度其の傾きで0を横切る

山と谷が格子点の上ではなく間に来るので、格子がずっと見え難くなります。
ノイズの値は0を中心にしていて、約半分が負です。

8.5.2 2Dでは

2Dでは、各格子点がランダムな単位ベクトル𝐠(勾配、傾きの2D版)を持ちます。
角の「直線」は傾いた平面になります。
角の勾配と、其の角から点へのベクトルの内積です(線型代数の第4章)。
𝐢の角から点へのベクトルは𝐟、𝐢+(1,0)の角からは𝐟−(1,0)、という具合です:

a=𝐠00⋅𝐟,b=𝐠10⋅(𝐟−(1,0)),c=𝐠01⋅(𝐟−(0,1)),d=𝐠11⋅(𝐟−(1,1))

そしてバリューノイズと同じ双線型の混ぜ方をします。

// 格子点のランダムな単位ベクトル
vec2 gradient(vec2 cell) {
  float a = hash21(cell) * 6.2832;
  return vec2(cos(a), sin(a));
}

// グラディエントノイズ(パーリンノイズの仲間):約-0.7〜0.7
float gradientNoise(vec2 p) {
  vec2 i = floor(p);
  vec2 f = fract(p);
  float a = dot(gradient(i), f);
  float b = dot(gradient(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
  float c = dot(gradient(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
  float d = dot(gradient(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
  vec2 u = quintic(f);
  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

ランダムな単位ベクトルは、0から2πまでのランダムな角度を cos と sin で方向にした物です。
単位ベクトルの勾配なら、結果は約−0.71〜0.71(±22)に収まります。

勾配が𝐠00=(1,0)、𝐠10=(0,1)、𝐠01=(−1,0)、𝐠11=(0,−1)で、同じ点𝐟=(0.25,0.5)での計算例:

a=(1,0)⋅(0.25,0.5)=0.25b=(0,1)⋅(−0.75,0.5)=0.5c=(−1,0)⋅(0.25,−0.5)=−0.25d=(0,−1)⋅(−0.75,−0.5)=0.5mix(0.25,0.5,0.10352)=0.27588mix(−0.25,0.5,0.10352)=−0.17236n=0.27588+(−0.17236−0.27588)×0.5=0.05176

バリューノイズ(左)とグラディエントノイズ(右、0〜1に収まる様に 0.5 + 0.7 * n で表示)を並べてみます:

左:正方形の構造が見えるバリューノイズ。右:丸く流れる様な形のグラディエントノイズ

グラディエントノイズにはブロックの様な構造が無く、丸く流れる形に見えます。
殆どのエフェクトは此のノイズの上に作られています。

Perlinのノイズは、1985年には格子点毎のハッシュが重かった為、置換表を使って小さな固定の表から勾配を選びます(2002年の改良版では3Dで12方向だけです)。此方はハッシュを使い、どの方向でも選べますが、見た目は同じです。シンプレックスノイズ(同じくPerlin、2001年)は正方形の代わりに三角形を使うので、高い次元で安く、方向による模様も更に少なくなります。上のグラディエントノイズの3D版は、4つの角の代わりに立方体の8つの角を見ます。

8.6 フラクタルノイズ:fBm

ノイズ1層には、1つの大きさの細部しかありません。
1単位に4マスなら、塊は約4分の1単位の幅です。
本物の表面には全ての大きさの細部があります。
山脈には山があり、各山には尾根があり、各尾根には岩があります。
其の技は、周波数を2倍(細部を小さく)、振幅を半分(弱く)にしたノイズを何層も足す事です:

fbm(𝐩)=∑k=0N−1gka0n(ℓk𝐩)

3つのノイズの曲線:1つ目はゆっくりで大きく、2つ目は2倍速く半分の高さ、3つ目は4倍速く4分の1の高さ。下の其の和には大きな形と細かい細部の両方がある

名前は音楽と物理から来ています:

  • 各層はオクターブです。周波数を2倍にするのは音楽の1オクターブだからです。
  • ℓはラクナリティで、周波数を大きくする倍率です(普通は2)。
  • gはゲイン(又はパーシステンス)で、振幅を小さくする倍率です(普通は0.5)。
  • 其の和をfBm(fractional Brownian motion、非整数ブラウン運動)と呼びます。g=0.5、ℓ=2で、多くの自然の表面と同じ統計的な「粗さ」になります。

6オクターブでa0=0.5なら、振幅は0.5+0.25+0.125+0.0625+0.03125+0.015625=0.984375なので、グラディエントノイズのfBmは1オクターブと大体同じ範囲に収まります。

// fBm(非整数ブラウン運動):オクターブを重ねる
float fbm(vec2 p, int octaves) {
  float sum = 0.0;
  float amp = 0.5;
  for (int i = 0; i < octaves; i++) {
    sum += amp * gradientNoise(p);
    p = rot(0.6) * p * 2.0 + vec2(17.1, 9.3);  // 周波数を2倍に(回してずらし、格子を隠す)
    amp *= 0.5;                                // 振幅を半分に
  }
  return sum;
}

ループの中の二つの細かい点:

  • rot(0.6)(第7章)は各オクターブを約34°回します。此れが無いと全てのオクターブの格子が揃い、格子の線が足し合わさって縦横の構造が見えてしまいます。
  • + vec2(17.1, 9.3) は各オクターブをずらします。全てのオクターブは格子点(0, 0)で0になるので、ずらさないと原点で全部揃って0になります。

octaves は引数なので、ループの長さは定数ではありません。
デスクトップのGLSLでは問題ありませんが、GLSL ES 1.00(WebGL 1)ではループの長さは定数でなければなりません。

左から、同じノイズの1、2、4、8オクターブです(0.5 + fbm で表示):

灰色のノイズの4本の帯:1オクターブは柔らかい塊、2、4、8オクターブと細部が増えて雲の様になる

オクターブ1つ毎にノイズの評価(ハッシュ4回と内積4回)が1回増えるので、オクターブ数は細部と速さの直接の取引です。
一番小さな細部がピクセルより小さくなるオクターブは無駄な仕事です。
1単位に3マス、1単位300ピクセルなら、7オクターブ目は既に1マス約1.5ピクセルです。

8.7 ドメインワーピング

fBmは雲や粗い表面の様に見えますが、少し単調です。
Inigo Quilezによる美しい技があります。
ノイズを読む前に、ノイズを使って座標を動かすのです。
第7章で、座標を変えると図形が変わる事は分かっています。
ここでは其の変化がピクセル毎に違うので、模様が液体の様に曲がり、渦を巻きます:

n(𝐩)=fbm(𝐩+4𝐰(𝐩)),𝐰(𝐩)=(fbm(𝐩),fbm(𝐩+𝐨))

ずらす量𝐨は、2つの成分を互いに無関係にする為だけの物です。
そして、此れを2回行えます:

void main() {
  vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
  vec2 q = p * 2.0;
  float t = uTime * 0.1;

  // 1段目:fBmでずらす量を作る
  vec2 w1 = vec2(fbm(q + vec2(0.0, 0.0) + t, 5), fbm(q + vec2(5.2, 1.3) - t, 5));
  // 2段目:ずらした座標で、もう一度ずらす量を作る
  vec2 w2 = vec2(fbm(q + 4.0 * w1 + vec2(1.7, 9.2), 5), fbm(q + 4.0 * w1 + vec2(8.3, 2.8), 5));
  // 最後に、2回ずらした座標でfBmを読む
  float n = fbm(q + 4.0 * w2, 5);

  // 色付け:ずらした量も色に使う
  vec3 color = mix(vec3(0.10, 0.20, 0.35), vec3(0.85, 0.55, 0.25), clamp(0.5 + 1.5 * n, 0.0, 1.0));
  color = mix(color, vec3(0.05, 0.05, 0.15), clamp(length(w1) * 1.2, 0.0, 1.0));
  color = mix(color, vec3(0.95, 0.90, 0.80), clamp(2.0 * w2.x * w2.x, 0.0, 1.0));
  out_color = vec4(color, 1.0);
}

大理石やガス惑星の表面の様な、渦を巻くオレンジ・青・クリーム色の模様

大理石、錆、煙、木星の表面の様に見えます。
t は1段目をゆっくり逆向きに滑らせるので、模様が時間と共に流れます。
重い処理です。
5オクターブのfBmを5回、ピクセル毎にノイズの評価が25回です。
ゲームなら毎フレーム計算せず、一度テクスチャに計算しておくでしょう。

8.8 セルラーノイズ(ボロノイ)

全く違う種類のノイズです。
平面にランダムな点を撒き(格子のマス毎に1つ)、各ピクセルで一番近い点までの距離を測ります。
結果は細胞、鱗、ひび割れた泥、石垣、キリンの模様の様に見えます。

格子のマス3×3、各マスにランダムな点が1つ。中央のマスのピクセルfが9つの点全てと結ばれ、一番近いのがF1、2番目がF2

各マスの点は、自分のマスの中のランダムな位置にあります(hash22 から)。
ピクセルに一番近い点は自分のマスにあるか、隣の8マスのどれかにあるので、9つ全てを調べます。
此れは第7章7.5の「隣のマスを調べる」です。
ピクセルは自分のマスしか知りませんが、必要な時には何時でも隣のマスの乱数を計算出来ます。

一番小さい2つの距離を残します。
一番近い点までのF1と、2番目に近い点までのF2です。

// セルラー(ボロノイ)ノイズ:一番近い特徴点と2番目に近い特徴点までの距離
vec2 voronoi(vec2 p, float t) {
  vec2 i = floor(p);
  vec2 f = fract(p);
  float d1 = 8.0;
  float d2 = 8.0;
  for (int y = -1; y <= 1; y++) {
    for (int x = -1; x <= 1; x++) {
      vec2 o = vec2(float(x), float(y));
      vec2 h = hash22(i + o);
      vec2 pt = o + 0.5 + 0.4 * sin(t + 6.2832 * h);  // 隣のマスの特徴点(時間で揺れる)
      float d = length(pt - f);
      if (d < d1) {
        d2 = d1;
        d1 = d;
      } else if (d < d2) {
        d2 = d;
      }
    }
  }
  return vec2(d1, d2);
}

全ての位置は現在のマスの角 i から見た物です。
隣のマス o の点は o + (0〜1の何か) にあり、ピクセルは f にあります。
点は sin(t + 6.2832 * h) で其々の位相で小さな円を描いて動き、マスの0.1〜0.9の範囲に留まります。
其の為マスの境界から十分離れていて、3×3の探索で必ず一番近い点が見つかります。

F1とF2の組み合わせ方で、見た目が大きく変わります:

void main() {
  vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
  vec2 q = p * 3.0;
  float x = gl_FragCoord.x / uResolution.x;

  vec2 v = voronoi(q, uTime);
  vec3 color;
  if (x < 1.0 / 3.0) {
    color = vec3(v.x);                                       // 左:F1(一番近い点までの距離)
  } else if (x < 2.0 / 3.0) {
    color = vec3(1.0 - smoothstep(0.0, 0.06, v.y - v.x));    // 中央:F2 - F1 が小さい所=境界
  } else {
    color = vec3(v.y - v.x);                                 // 右:F2 - F1
  }
  color *= smoothstep(0.0, 0.004, abs(fract(x * 3.0 + 0.5) - 0.5));
  out_color = vec4(color, 1.0);
}

左:特徴点で暗く、外に行く程明るい。中央:黒地にマスの境界の白い線。右:中央が明るく境界が暗い、石の様なセル

  • F1だけ:全ての点で暗く、間で明るい。泡、斑点、細胞に向いています。
  • F2−F1は、2つの点が同じ近さの所、つまり2つのセルの境界で丁度0になります。閾値で切ると境界が描けます(ひび、石垣、トンボの羽)。
  • F2−F1其の物は、各セルを小さな丘にします。石、鱗、石畳です。

中央の帯をよく見て下さい。
2つの特徴点が近い所では線が太くなっています。
F2−F1は境界までの本当の距離ではなく、2つの点が近いとゆっくり変わるのです。
正確な境界線には、一番近い2つの点の垂直二等分線までの距離を測る2回目のパスが必要です(課題7)。

8.9 エフェクトでのノイズ

ノイズだけでは灰色の形に過ぎません。
腕の見せ所は、其れを何に繋げるかです。

8.9.1 ディゾルブ

ディゾルブは、物体が燃え尽きたりテレポートで消えたりするエフェクトです。
各ピクセルでノイズを閾値と比べ、ノイズの方が低い所では、ピクセルは既に消えています。
閾値を0から1に動かすと、図形は「弱い」所から外へ向かって食われて行き、ノイズが閾値に近いピクセルには光る縁が付きます:

void main() {
  vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
  vec3 color = vec3(0.10, 0.10, 0.12);

  // 溶けて消えるカード(第6章の角丸の箱)
  float card = sdRoundBox(p, vec2(0.7, 0.5), 0.08);
  vec3 cardColor = mix(vec3(0.25, 0.45, 0.85), vec3(0.55, 0.80, 1.00), 0.5 + 0.5 * p.y);

  float n = 0.5 + fbm(p * 3.0, 5);                 // 0〜1程度のノイズ
  float threshold = 0.5 + 0.45 * sin(uTime * 0.8); // 時間で上下する閾値
  float edge = n - threshold;                      // 負の所は既に燃え尽きた

  float alive = fill(card) * smoothstep(0.0, 0.01, edge);
  float burn = fill(card) * (1.0 - smoothstep(0.0, 0.06, abs(edge)));  // 燃えている縁
  color = mix(color, cardColor, alive);
  color += burn * vec3(1.0, 0.45, 0.1) * 1.5;
  out_color = vec4(color, 1.0);
}

穴が開いて行く青いカード。穴の周りにオレンジ色に光る縁

光る縁は第6章の輪郭の技(abs(d) - w)を、edge を距離と見なして使った物です。
0の線が境界になる関数なら、どれでも其の様に扱えます。

8.9.2 炎

簡単な炎です。
時間と共に上へ流れるfBm(第7章の逆の規則により、yから uTime を引くと模様は上に動きます)を、下が太く上が細い形で切り抜き、「熱さ」で色を付けます:

void main() {
  vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;

  // 炎:上に流れるノイズを、下が太く上が細い形で切り抜く
  vec2 q = vec2(p.x * 2.5, p.y * 1.5 - uTime * 1.5);
  float n = 0.5 + fbm(q, 5);
  float shape = 1.0 - length(vec2(p.x * (1.8 + 1.2 * max(p.y, 0.0)), (p.y + 0.6) * 0.7));  // 上程細い
  float heat = clamp(shape * 1.6 + (n - 0.5) * 1.2 - 0.2, 0.0, 1.0);

  // 熱さを色に:黒→赤→オレンジ→黄色→白
  vec3 color = vec3(1.5 * heat, 1.5 * heat * heat, heat * heat * heat * heat);
  out_color = vec4(min(color, vec3(1.0)), 1.0);
}

黒地に、中心が白く熱いオレンジと黄色の炎

色の変化はチャンネル毎に heat の違う累乗を使っています。
赤が最初に上がり、緑が続き(2乗)、青は一番熱い所でだけ現れる(4乗)ので、本物の炎の様に冷たい所は暗い赤、熱い所は白になります。

8.10 全部まとめる:夕焼けの風景

此の章の最終的な playground.glsl は、風景全体をノイズで描きます。
星はハッシュ、太陽と其の光彩は円のSDF、雲はずらしたfBm、3層の山は1DのfBmです。
第6章と第7章の関数は残し、時計の関数(bricks、sdHand、sdHands)は削除して、此の章のノイズの関数を repeatAngle の後に追加します。

// 山の稜線の高さ:xだけの関数(1Dのノイズとして、yに層毎の番号を入れる)
float ridge(float x, float layer) {
  return fbm(vec2(x, layer * 7.3), 5);
}

void main() {
  // 中心が原点、縦が-1〜1になる座標(第5章)
  vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
  float t = uTime;

  // 空:夕焼けのグラデーション
  vec3 color = mix(vec3(0.98, 0.55, 0.30), vec3(0.12, 0.10, 0.30), smoothstep(-0.3, 1.0, p.y));

  // 星:画素の格子毎の乱数で、ごく一部だけ光らせる
  vec2 starCell = floor(gl_FragCoord.xy / 3.0);
  float star = step(0.996, hash21(starCell));
  float twinkle = 0.6 + 0.4 * sin(t * 3.0 + 6.2832 * hash21(starCell + 71.0));
  color += star * twinkle * smoothstep(0.3, 0.9, p.y) * vec3(0.9, 0.9, 1.0);

  // 太陽:円と、距離で弱くなる光彩(第6章)
  vec2 sunPos = vec2(0.45, -0.05);
  float sun = sdCircle(p - sunPos, 0.18);
  color += vec3(1.0, 0.6, 0.3) * 0.35 * exp(-4.0 * max(sun, 0.0));
  color = paint(color, vec3(1.0, 0.92, 0.70), sun);

  // 雲:ゆっくり流れる、ずらしたfBm
  vec2 cq = vec2(p.x * 1.2 + t * 0.03, p.y * 3.0);
  vec2 warp = vec2(fbm(cq + vec2(0.0, t * 0.05), 4), fbm(cq + vec2(3.7, 1.9), 4));
  float cloud = smoothstep(-0.05, 0.4, fbm(cq + 1.5 * warp, 6) + 0.25 * (p.y - 0.3));
  cloud *= smoothstep(0.0, 0.35, p.y);                              // 地平線の近くには出さない
  vec3 cloudColor = mix(vec3(0.95, 0.55, 0.45), vec3(0.35, 0.25, 0.45), smoothstep(0.2, 0.9, p.y));
  color = mix(color, cloudColor, cloud * 0.85);

  // 山:奥から手前へ3層。手前程暗く、大きく、ギザギザに
  for (int i = 0; i < 3; i++) {
    float k = float(i);
    float h = -0.15 - 0.22 * k + (0.35 - 0.05 * k) * ridge(p.x * (1.0 + 0.6 * k) + 3.1 * k, k);
    vec3 hill = mix(vec3(0.45, 0.30, 0.45), vec3(0.06, 0.05, 0.10), k / 2.0);
    hill = mix(hill, vec3(0.98, 0.55, 0.30), 0.25 * (1.0 - k / 2.0));  // 遠くは空の色が混ざる(霞)
    color = paint(color, hill, p.y - h);
  }

  out_color = vec4(color, 1.0);
}

夕焼け:上に行く程紫になるオレンジの空に星と薄い雲、3層の山のシルエットの向こうに淡い太陽

各部分の仕組み:

  • 星:step(0.996, hash21(...)) は3×3ピクセルのマスの約0.4%で1になります。同じマスの2つ目のハッシュ(違う数になる様に71ずらす)で、星毎に瞬きの位相を変えています。ホワイトノイズを控えめに使った例です。
  • 山:ridge(x, layer) は2DのfBmを水平な線に沿って読みます。層毎に違うyを使うと、3つの無関係な稜線になります。ピクセルが稜線より下なら山の中なので、p.y - h は paint にとって符号付き距離の様に働きます。正確な距離ではありませんが、fill に必要なのは符号と傾きだけです。
  • 奥行き:奥の層程明るく空の色が混ざり、手前の層は殆ど黒です。此の「空気遠近法」は画家が距離を表す方法で、3Dゲームの霧も同じ事をしています(第14章)。
  • 雲:ドメインワーピングしたfBmを横に伸ばし(p.y * 3.0 で縦に潰す)、t * 0.03 で流し、地平線の近くでは消しています。

テスターを1分程動かしたままにすると、星が瞬く中、雲がゆっくり流れて形を変えます。

8.11 此の章の新しいGLSLの機能

機能 何をするか
uint、uvec2 符号無しの32ビット整数(GLSL 1.30以降)
16u、0x7feb352du 符号無し整数のリテラル(10進数と16進数)
^、&、>>、<< Cと同じビット毎のXOR、AND、右シフト、左シフト
uint の桁あふれ Cと同じく2の32乗で割った余りになる
ivec2(v)、uvec2(v) 符号付き・符号無しの整数ベクトルに変換
float(u) 整数をfloatに変換
exp(x) ex(第6章から)
回数が変数の for デスクトップのGLSLでは使えるが、GLSL ES 1.00では使えない

8.12 実際のゲームやエンジンでのノイズ

  • 地形。 オープンワールドの高さマップはfBm(鋭い尾根には 1.0 - abs(noise) のリッジノイズがよく使われます)から始まり、其の後で侵食や手作業の編集が加えられます。Minecraftのワールド生成は、2Dと3Dのパーリン風ノイズの層で作られています。
  • 雲と霧。 Horizon Zero Dawnのボリュメトリッククラウド(Andrew Schneider「The Real-time Volumetric Cloudscapes of Horizon Zero Dawn」、SIGGRAPH 2015)は、3Dテクスチャに保存したパーリンノイズとウォーリー(セルラー)ノイズを組み合わせています。多くのゲームの動く霧や煙は、体積の中でfBmをスクロールさせています。
  • 水。 波紋やコースティクスは、スクロールするノイズやボロノイ模様である事が多いです。岸辺の泡は、ディゾルブの様な縁を付けて閾値で切ったノイズです。
  • 炎、魔法、ディゾルブ。 8.9.1のディゾルブは、ゲームで最もよく使われるシェーダーエフェクトの一つです。燃え尽きる敵、テレポートのエフェクト、フェードインするステージ。ノイズは計算せず、テクスチャから読む事が多いです。
  • マテリアル。 プロシージャルマテリアルのツール(Substance Designer、Blender、Houdini)の錆、汚れ、傷、大理石、木目は、ノイズ、fBm、ワーピング、ボロノイをマスクで混ぜた物です。
  • ディザリングとサンプリング。 ハッシュやブルーノイズは、グラデーションのバンディングを崩し、柔らかい影やアンビエントオクルージョン(第14章)のサンプルを散らします。バンディングを、目が受け入れ易い細かい粒に置き換えるのです。
  • 事前計算したノイズ。 速さの為、ゲームはノイズをタイル出来るテクスチャに保存し、毎フレームハッシュを計算する代わりにテクスチャの読み込み1回(第11章)で済ませる事がよくあります。無限に繰り返さない細部やアニメーションが必要な時は、シェーダーで計算する方が良いです。

8.13 まとめ

  • シェーダーには rand() がありません。入力をランダムに見えるけれど再現可能な数に変えるハッシュを使います。
  • 古典的な fract(sin(dot(...)) * 43758.5453) は sin の精度に依存し、大きな入力で壊れます。uint、シフト、XOR、掛け算を使う整数のハッシュの方が良く、速さも同程度です。
  • ハッシュの24ビットを float(h >> 8u) / 16777216.0 でfloatにします。
  • バリューノイズは格子の角の乱数を混ぜ、グラディエントノイズはランダムな傾きを混ぜて格子をずっと上手く隠します。5次の6t5−15t4+10t3で混ぜます。
  • fBmは周波数を2倍(ラクナリティ)、振幅を半分(ゲイン)にしたオクターブを足します。各オクターブを回してずらします。
  • ドメインワーピングはノイズをノイズの座標に入れます。ボロノイは隣の3×3のマスのランダムな点までの距離を測り、F1とF2を返します。
  • エフェクトは、ノイズを何に繋げるかで決まります。閾値(ディゾルブ)、色の変化(炎)、高さ(山)、ずらし(ワーピング)。

8.14 課題

  1. ★ 角の値がa=0、b=1、c=1、d=0の時、5次の曲線を使って𝐟=(0.5,0.5)と𝐟=(0.25,0)でのバリューノイズを計算して下さい。(1つ目は丁度0.5になる筈です。)
  2. ★ gradientNoise の6オクターブのfBmで、1ピクセルに hashU は何回呼ばれますか?8.7のドメインワーピングの1ピクセルでは?
  3. ★★ 古いテレビのエフェクトを作って下さい。2×2ピクセルのマスで1秒に30回変わるホワイトノイズに、暗い横の走査線と、ゆっくり流れる明るい帯を付けます。テスターを何時間動かしても模様が出ない様にして下さい(整数のハッシュを使い、必要ならフレーム番号に fract か mod を使います)。
  4. ★★ fbm をリッジノイズに変えて下さい。amp * gradientNoise(p) の代わりに amp * (1.0 - abs(gradientNoise(p)))(尾根を鋭くするなら其の2乗)を足します。風景の手前の山の層に使って下さい。手前の山は鋭い岩山になり、奥の層は柔らかいままの筈です。
  5. ★★ 木目のテクスチャを描いて下さい。点からの距離(木の幹の年輪の様に)を取り、少しfBmを足し、fract(distance * 10.0) で明るい茶色から暗い茶色への色の変化を付けます。年輪は完全な円ではなく、揺らいでいる筈です。
  6. ★★ ボロノイの各セルを其々のランダムな色で塗って下さい。voronoi を、一番近い点のマスの番号(i + o)も返す様に変え(例えば out vec2 引数で)、其れに hash22 か第7章のコサインのパレットを使います。セルはステンドグラスの様に見え、F2−F1の境界が暗い鉛の線になる筈です。
  7. ★★★ ボロノイの境界の太い線を直して下さい。一番近い点𝐦を見つけた後、もう一度隣のマスを(念の為5×5で)巡り、他の全ての点𝐫について垂直二等分線までの距離 dot((m + r) * 0.5 - f, normalize(r - m)) を計算し、一番小さい物を残します。8.8の中央の帯と比べて下さい。線が何処でも同じ太さになる筈です。線型代数の第4章の射影を使って、何故此れが境界までの本当の距離なのか説明して下さい。

DIFFをダウンロード

ファイル: a/shader/playground.glsl | b/shader/playground.glsl

前新
@@ -64,18 +64,6 @@ float fillPx(float d) {
64 return 1.0 - smoothstep(-px, px, d); 64 return 1.0 - smoothstep(-px, px, d);
65 } 65 }
66   66  
67 // レンガの壁(第7章のタイル)    
68 vec3 bricks(vec2 p) {    
69 vec2 size = vec2(0.4, 0.2);    
70 vec2 q = p / size;    
71 q.x += 0.5 * mod(floor(q.y), 2.0); // 1段おきに半分ずらす    
72 vec2 id = floor(q);    
73 vec2 f = (fract(q) - 0.5) * size;    
74 float d = sdRoundBox(f, size * 0.5 - 0.012, 0.02);    
75 float shade = 0.85 + 0.15 * cos(id.x * 2.7 + id.y * 5.1);    
76 return mix(vec3(0.55, 0.53, 0.50), vec3(0.70, 0.32, 0.22) * shade, fillPx(d));    
77 }    
78      
79 // 角度の繰り返し:pを一番近い区画の中心(x軸上)に回す 67 // 角度の繰り返し:pを一番近い区画の中心(x軸上)に回す
80 vec2 repeatAngle(vec2 p, float count) { 68 vec2 repeatAngle(vec2 p, float count) {
81 float sector = 6.2832 / count; 69 float sector = 6.2832 / count;
@@ -83,54 +71,142 @@ vec2 repeatAngle(vec2 p, float count) {
83 return rot(-k * sector) * p; 71 return rot(-k * sector) * p;
84 } 72 }
85   73  
86 // 針:12時の方向を向いたカプセルを、時計回りに回す 74 // ---- 第8章:乱数とノイズ ----
87 // 座標をrot(+a)で回すと、図形は-a、つまり時計回りに回る 75  
88 float sdHand(vec2 p, float turn, float back, float len, float width) { 76 // 整数ハッシュ(Chris Wellonsのlowbias32):32ビットをよく掻き混ぜる
89 vec2 q = rot(turn * 6.2832) * p; 77 uint hashU(uint x) {
90 return sdSegment(q, vec2(0.0, -back), vec2(0.0, len)) - width; 78 x ^= x >> 16u;
    79 x *= 0x7feb352du;
    80 x ^= x >> 15u;
    81 x *= 0x846ca68bu;
    82 x ^= x >> 16u;
    83 return x;
91 } 84 }
92   85  
93 // 時針と分針を1つの距離に(影にも使う) 86 // 格子点(整数の座標)から0〜1の乱数を1つ
94 float sdHands(vec2 p, float t) { 87 float hash21(vec2 cell) {
95 float h = sdHand(p, fract(t / 43200.0), 0.08, 0.36, 0.028); // 時針:12時間で1周 88 uvec2 q = uvec2(ivec2(cell)); // 負の数もビットのまま変換される
96 float m = sdHand(p, fract(t / 3600.0), 0.10, 0.54, 0.018); // 分針:1時間で1周 89 return float(hashU(q.x + hashU(q.y)) >> 8u) / 16777216.0;
97 return min(h, m);    
98 } 90 }
99   91  
100 void main() { 92 // 格子点から0〜1の乱数を2つ
101 // 中心が原点、縦が-1〜1になる座標(第5章) 93 vec2 hash22(vec2 cell) {
102 vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y; 94 uvec2 q = uvec2(ivec2(cell));
    95 uint h = hashU(q.x + hashU(q.y));
    96 return vec2(float(h >> 8u), float(hashU(h) >> 8u)) / 16777216.0;
    97 }
    98  
    99 // 5次の補間曲線 6t^5 - 15t^4 + 10t^3(両端で1階と2階の微分が0)
    100 vec2 quintic(vec2 t) {
    101 return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
    102 }
103   103  
104 // 10時10分0秒から始まる時刻(秒) 104 // バリューノイズ:格子点の乱数を滑らかに補間(0〜1)
105 float t = uTime + 10.0 * 3600.0 + 10.0 * 60.0; 105 float valueNoise(vec2 p) {
    106 vec2 i = floor(p);
    107 vec2 f = fract(p);
    108 float a = hash21(i);
    109 float b = hash21(i + vec2(1.0, 0.0));
    110 float c = hash21(i + vec2(0.0, 1.0));
    111 float d = hash21(i + vec2(1.0, 1.0));
    112 vec2 u = quintic(f);
    113 return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
    114 }
106   115  
107 // 背景:少し暗くしたレンガの壁 116 // 格子点のランダムな単位ベクトル
108 vec3 color = bricks(p) * 0.55; 117 vec2 gradient(vec2 cell) {
    118 float a = hash21(cell) * 6.2832;
    119 return vec2(cos(a), sin(a));
    120 }
109   121  
110 // 時計の影 122 // グラディエントノイズ(パーリンノイズの仲間):約-0.7〜0.7
111 float shadow = sdCircle(p - vec2(0.04, -0.06), 0.8); 123 float gradientNoise(vec2 p) {
112 color *= 1.0 - 0.6 * (1.0 - smoothstep(-0.05, 0.1, shadow)); 124 vec2 i = floor(p);
    125 vec2 f = fract(p);
    126 float a = dot(gradient(i), f);
    127 float b = dot(gradient(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
    128 float c = dot(gradient(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
    129 float d = dot(gradient(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
    130 vec2 u = quintic(f);
    131 return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
    132 }
113   133  
114 // 枠と文字盤 134 // fBm(非整数ブラウン運動):オクターブを重ねる
115 float rim = sdCircle(p, 0.8); 135 float fbm(vec2 p, int octaves) {
116 float face = sdCircle(p, 0.72); 136 float sum = 0.0;
117 color = paint(color, vec3(0.20, 0.18, 0.22), rim); 137 float amp = 0.5;
118 color = paint(color, vec3(0.96, 0.93, 0.85), face); 138 for (int i = 0; i < octaves; i++) {
    139 sum += amp * gradientNoise(p);
    140 p = rot(0.6) * p * 2.0 + vec2(17.1, 9.3); // 周波数を2倍に(回してずらし、格子を隠す)
    141 amp *= 0.5; // 振幅を半分に
    142 }
    143 return sum;
    144 }
119   145  
120 // 目盛り:60個の小さい物と12個の大きい物 146 // セルラー(ボロノイ)ノイズ:一番近い特徴点と2番目に近い特徴点までの距離
121 float minuteTick = sdBox(repeatAngle(p, 60.0) - vec2(0.65, 0.0), vec2(0.025, 0.004)); 147 vec2 voronoi(vec2 p, float t) {
122 float hourTick = sdBox(repeatAngle(p, 12.0) - vec2(0.62, 0.0), vec2(0.06, 0.014)); 148 vec2 i = floor(p);
123 color = paint(color, vec3(0.25), minuteTick); 149 vec2 f = fract(p);
124 color = paint(color, vec3(0.12), hourTick); 150 float d1 = 8.0;
    151 float d2 = 8.0;
    152 for (int y = -1; y <= 1; y++) {
    153 for (int x = -1; x <= 1; x++) {
    154 vec2 o = vec2(float(x), float(y));
    155 vec2 h = hash22(i + o);
    156 vec2 pt = o + 0.5 + 0.4 * sin(t + 6.2832 * h); // 隣のマスの特徴点(時間で揺れる)
    157 float d = length(pt - f);
    158 if (d < d1) {
    159 d2 = d1;
    160 d1 = d;
    161 } else if (d < d2) {
    162 d2 = d;
    163 }
    164 }
    165 }
    166 return vec2(d1, d2);
    167 }
125   168  
126 // 針の影、時針と分針 169 // 山の稜線の高さ:xだけの関数(1Dのノイズとして、yに層毎の番号を入れる)
127 color *= 1.0 - 0.3 * (1.0 - smoothstep(-0.02, 0.03, sdHands(p - vec2(0.015, -0.025), t))); 170 float ridge(float x, float layer) {
128 color = paint(color, vec3(0.12, 0.12, 0.15), sdHands(p, t)); 171 return fbm(vec2(x, layer * 7.3), 5);
    172 }
129   173  
130 // 秒針:1秒毎にカチッと動く 174 void main() {
131 float second = sdHand(p, fract(floor(t) / 60.0), 0.15, 0.6, 0.006); 175 // 中心が原点、縦が-1〜1になる座標(第5章)
132 second = min(second, sdCircle(p, 0.035)); 176 vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
133 color = paint(color, vec3(0.85, 0.15, 0.15), second); 177 float t = uTime;
    178  
    179 // 空:夕焼けのグラデーション
    180 vec3 color = mix(vec3(0.98, 0.55, 0.30), vec3(0.12, 0.10, 0.30), smoothstep(-0.3, 1.0, p.y));
    181  
    182 // 星:画素の格子毎の乱数で、ごく一部だけ光らせる
    183 vec2 starCell = floor(gl_FragCoord.xy / 3.0);
    184 float star = step(0.996, hash21(starCell));
    185 float twinkle = 0.6 + 0.4 * sin(t * 3.0 + 6.2832 * hash21(starCell + 71.0));
    186 color += star * twinkle * smoothstep(0.3, 0.9, p.y) * vec3(0.9, 0.9, 1.0);
    187  
    188 // 太陽:円と、距離で弱くなる光彩(第6章)
    189 vec2 sunPos = vec2(0.45, -0.05);
    190 float sun = sdCircle(p - sunPos, 0.18);
    191 color += vec3(1.0, 0.6, 0.3) * 0.35 * exp(-4.0 * max(sun, 0.0));
    192 color = paint(color, vec3(1.0, 0.92, 0.70), sun);
    193  
    194 // 雲:ゆっくり流れる、ずらしたfBm
    195 vec2 cq = vec2(p.x * 1.2 + t * 0.03, p.y * 3.0);
    196 vec2 warp = vec2(fbm(cq + vec2(0.0, t * 0.05), 4), fbm(cq + vec2(3.7, 1.9), 4));
    197 float cloud = smoothstep(-0.05, 0.4, fbm(cq + 1.5 * warp, 6) + 0.25 * (p.y - 0.3));
    198 cloud *= smoothstep(0.0, 0.35, p.y); // 地平線の近くには出さない
    199 vec3 cloudColor = mix(vec3(0.95, 0.55, 0.45), vec3(0.35, 0.25, 0.45), smoothstep(0.2, 0.9, p.y));
    200 color = mix(color, cloudColor, cloud * 0.85);
    201  
    202 // 山:奥から手前へ3層。手前程暗く、大きく、ギザギザに
    203 for (int i = 0; i < 3; i++) {
    204 float k = float(i);
    205 float h = -0.15 - 0.22 * k + (0.35 - 0.05 * k) * ridge(p.x * (1.0 + 0.6 * k) + 3.1 * k, k);
    206 vec3 hill = mix(vec3(0.45, 0.30, 0.45), vec3(0.06, 0.05, 0.10), k / 2.0);
    207 hill = mix(hill, vec3(0.98, 0.55, 0.30), 0.25 * (1.0 - k / 2.0)); // 遠くは空の色が混ざる(霞)
    208 color = paint(color, hill, p.y - h);
    209 }
134   210  
135 out_color = vec4(color, 1.0); 211 out_color = vec4(color, 1.0);
136 } 212 }


コメント(0)

まだコメントはありません。