8. 乱数とノイズ
此れまで描いた物は全て完全に規則的でした。
正確な円、真っ直ぐなレンガ、同じ目盛りが並んだ時計。
自然はそうではありません。
雲、岩、木、炎、水、地形、錆には構造がありますが、全く同じに見える場所は二つとありません。
此の章では、シェーダーで其の様な「制御された不規則さ」を作る方法を学びます。
先ず乱数、次にノイズ、つまり滑らかに変化する乱数です。
GPUに何故 rand() が全く無いのかから始め、整数のビット演算でちゃんとしたハッシュ関数を作り、其れからバリューノイズ、グラディエントノイズ(パーリンノイズの仲間)、フラクタルノイズ(fBm)、ドメインワーピング、セルラー(ボロノイ)ノイズを作ります。
最後は、どのゲームでも見るエフェクト(ディゾルブと炎)と、山・雲・星が全てノイズで出来た夕焼けの風景です。
此れまで通り、変更するのは shader/playground.glsl だけです。
第6章と第7章の関数(sdCircle、sdRoundBox、fill、paint、rot 等)はファイルの先頭にそのまま残っています。
8.1 何故rand()が無いのか
Cでは rand() を呼ぶと毎回違う数が返って来ます。
其れは rand() が隠れた状態(シード)を持っていて、呼ぶ度に其れが変わるからです。
フラグメントシェーダーには其れが出来ません。
何十万ものフラグメントシェーダーが決まった順番無しに同時に実行され、其々が書き換える変数を共有する事は出来ません。
続きを始める「前回の呼び出し」が無いのです。
仮にあったとしても、GPUが偶々ピクセルを処理した順番によって、毎フレーム絵が変わってしまいます。
其の為、シェーダーには別の物が必要です。
入力(ピクセルの位置、マスの番号、時間)を受け取り、ランダムに見えるけれど同じ入力なら常に同じ数を返す関数です。
此の様な関数をハッシュ関数と呼びます。
同じ入力なら同じ出力、少しだけ違う入力なら全く違う出力です。
此れは制限ではなく、利点です。
マスの「乱数」がマスの番号だけで決まるので、マスの中の全てのピクセルが他と話さずに同じ値を得られ、絵は毎フレーム同じで、必要な時には何時でも隣のマスの乱数を求められます(ノイズは常に其れをします)。
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 で小数点以下の桁だけを残します。
其の桁は入力が少し変わると激しく変わるので、結果はランダムに見えます。
短くて大抵は動きますが、深刻な問題が二つあります。
- GPUが
sinをどれ位正確に計算するかに依存します。fractが残すのはサイン値の5桁目から10桁目です。32ビットのfloatの有効桁数は全部で約7桁しかなく、GPUは特に大きな引数でsinを其れより不正確に計算する事が許されています。或るGPUではランダムに見え、別のGPUでは縞が出て、スマートフォンでは酷い事になるかも知れません。 - 原点から遠いと壊れます。 入力が約だと、
dotは約になります。其の大きさでは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のハッシュです。
右側に点と縞の規則的な模様が現れています。
下半分は次の節の整数のハッシュで、入力は全く同じですが、両側ともランダムです。
(スクリーンショットは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と同じく、はみ出たビットは単に捨てられます。結果はをで割った余りです。掛け算は情報を下のビットから上のビットへ移し、次のシフトが其れを下に戻します。
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ビットを残し、で割ると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次元では、全ての整数で乱数を取り、との間では小数部分を使って mix でからへ混ぜます:
(線型補間)なら、整数毎に鋭い角のある直線になります(画像の赤)。
滑らかな曲線を使うと角が消えます(黄色):
8.4.2 曲線を選ぶ
滑らかな曲線は既に一つ知っています。
第6章のsmoothstepのです。
傾きが両端で0なので、角無く繋がります。
然し、其の曲率(2階微分、)はで6、で−6なので、整数毎に跳びます。
ノイズ其の物では見えませんが、ノイズを高さマップとして使い、其処からライティングを計算すると(第12章)、折れ目として現れます。
ライティングは傾きに依存し、其の傾きの変化が跳ぶからです。
Ken Perlinの改良版ノイズ(2002年)は、1階と2階の微分が両端で0になる5次の曲線を使います:
両端を確かめましょう。
、です。
微分はで、とで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つの角を持つ格子のマスの中にあります。
左下・右下・左上・右上の角の乱数を取り、下の辺と上の辺でxに沿って混ぜ、其の2つの結果をyに沿って混ぜます。
此れを双線型補間と呼びます(直線の代わりに曲線を使っていますが):
// バリューノイズ:格子点の乱数を滑らかに補間(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);
}
角の値が、、、で、マスの中の点がの計算例:
曲線を比べる為に、同じ乱数の格子(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);
}
線型補間では、格子の線に沿って明るい「星」と暗い「星」が見えます。
曲線で其れは消えますが、格子はまだ見えます。
バリューノイズは山と谷が丁度格子点にあり、其の周りの平らな所で正方形が見えてしまうのです。
グラディエントノイズが其れを解決します。
8.5 グラディエントノイズ
Ken Perlinのアイデア(1985年。後にアカデミー科学技術賞を受賞した研究)は、格子点にランダムな値ではなく、ランダムな傾きを置く事でした。
8.5.1 1Dでの考え方
全ての整数でノイズは0になり、其の点からランダムな傾きで出て行きます。
点の近くではノイズは直線の様に振る舞い、の近くではの様に振る舞います。
其の間では、此れらの直線を5次の曲線で混ぜます:
山と谷が格子点の上ではなく間に来るので、格子がずっと見え難くなります。
ノイズの値は0を中心にしていて、約半分が負です。
8.5.2 2Dでは
2Dでは、各格子点がランダムな単位ベクトル(勾配、傾きの2D版)を持ちます。
角の「直線」は傾いた平面になります。
角の勾配と、其の角から点へのベクトルの内積です(線型代数の第4章)。
の角から点へのベクトルは、の角からは、という具合です:
そしてバリューノイズと同じ双線型の混ぜ方をします。
// 格子点のランダムな単位ベクトル
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からまでのランダムな角度を cos と sin で方向にした物です。
単位ベクトルの勾配なら、結果は約−0.71〜0.71()に収まります。
勾配が、、、で、同じ点での計算例:
バリューノイズ(左)とグラディエントノイズ(右、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倍(細部を小さく)、振幅を半分(弱く)にしたノイズを何層も足す事です:
名前は音楽と物理から来ています:
- 各層はオクターブです。周波数を2倍にするのは音楽の1オクターブだからです。
- はラクナリティで、周波数を大きくする倍率です(普通は2)。
- はゲイン(又はパーシステンス)で、振幅を小さくする倍率です(普通は0.5)。
- 其の和をfBm(fractional Brownian motion、非整数ブラウン運動)と呼びます。、で、多くの自然の表面と同じ統計的な「粗さ」になります。
6オクターブでなら、振幅はなので、グラディエントノイズの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 で表示):
オクターブ1つ毎にノイズの評価(ハッシュ4回と内積4回)が1回増えるので、オクターブ数は細部と速さの直接の取引です。
一番小さな細部がピクセルより小さくなるオクターブは無駄な仕事です。
1単位に3マス、1単位300ピクセルなら、7オクターブ目は既に1マス約1.5ピクセルです。
8.7 ドメインワーピング
fBmは雲や粗い表面の様に見えますが、少し単調です。
Inigo Quilezによる美しい技があります。
ノイズを読む前に、ノイズを使って座標を動かすのです。
第7章で、座標を変えると図形が変わる事は分かっています。
ここでは其の変化がピクセル毎に違うので、模様が液体の様に曲がり、渦を巻きます:
ずらす量は、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つ)、各ピクセルで一番近い点までの距離を測ります。
結果は細胞、鱗、ひび割れた泥、石垣、キリンの模様の様に見えます。
各マスの点は、自分のマスの中のランダムな位置にあります(hash22 から)。
ピクセルに一番近い点は自分のマスにあるか、隣の8マスのどれかにあるので、9つ全てを調べます。
此れは第7章7.5の「隣のマスを調べる」です。
ピクセルは自分のマスしか知りませんが、必要な時には何時でも隣のマスの乱数を計算出来ます。
一番小さい2つの距離を残します。
一番近い点までのと、2番目に近い点までのです。
// セルラー(ボロノイ)ノイズ:一番近い特徴点と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の探索で必ず一番近い点が見つかります。
との組み合わせ方で、見た目が大きく変わります:
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);
}
- だけ:全ての点で暗く、間で明るい。泡、斑点、細胞に向いています。
- は、2つの点が同じ近さの所、つまり2つのセルの境界で丁度0になります。閾値で切ると境界が描けます(ひび、石垣、トンボの羽)。
- 其の物は、各セルを小さな丘にします。石、鱗、石畳です。
中央の帯をよく見て下さい。
2つの特徴点が近い所では線が太くなっています。
は境界までの本当の距離ではなく、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);
}
各部分の仕組み:
- 星:
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) |
(第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次ので混ぜます。
- fBmは周波数を2倍(ラクナリティ)、振幅を半分(ゲイン)にしたオクターブを足します。各オクターブを回してずらします。
- ドメインワーピングはノイズをノイズの座標に入れます。ボロノイは隣の3×3のマスのランダムな点までの距離を測り、とを返します。
- エフェクトは、ノイズを何に繋げるかで決まります。閾値(ディゾルブ)、色の変化(炎)、高さ(山)、ずらし(ワーピング)。
8.14 課題
- ★ 角の値が、、、の時、5次の曲線を使ってとでのバリューノイズを計算して下さい。(1つ目は丁度0.5になる筈です。)
- ★
gradientNoiseの6オクターブのfBmで、1ピクセルにhashUは何回呼ばれますか?8.7のドメインワーピングの1ピクセルでは? - ★★ 古いテレビのエフェクトを作って下さい。2×2ピクセルのマスで1秒に30回変わるホワイトノイズに、暗い横の走査線と、ゆっくり流れる明るい帯を付けます。テスターを何時間動かしても模様が出ない様にして下さい(整数のハッシュを使い、必要ならフレーム番号に
fractかmodを使います)。 - ★★
fbmをリッジノイズに変えて下さい。amp * gradientNoise(p)の代わりにamp * (1.0 - abs(gradientNoise(p)))(尾根を鋭くするなら其の2乗)を足します。風景の手前の山の層に使って下さい。手前の山は鋭い岩山になり、奥の層は柔らかいままの筈です。 - ★★ 木目のテクスチャを描いて下さい。点からの距離(木の幹の年輪の様に)を取り、少しfBmを足し、
fract(distance * 10.0)で明るい茶色から暗い茶色への色の変化を付けます。年輪は完全な円ではなく、揺らいでいる筈です。 - ★★ ボロノイの各セルを其々のランダムな色で塗って下さい。
voronoiを、一番近い点のマスの番号(i + o)も返す様に変え(例えばout vec2引数で)、其れにhash22か第7章のコサインのパレットを使います。セルはステンドグラスの様に見え、の境界が暗い鉛の線になる筈です。 - ★★★ ボロノイの境界の太い線を直して下さい。一番近い点を見つけた後、もう一度隣のマスを(念の為5×5で)巡り、他の全ての点について垂直二等分線までの距離
dot((m + r) * 0.5 - f, normalize(r - m))を計算し、一番小さい物を残します。8.8の中央の帯と比べて下さい。線が何処でも同じ太さになる筈です。線型代数の第4章の射影を使って、何故此れが境界までの本当の距離なのか説明して下さい。
ファイル: 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)
まだコメントはありません。
ログインするとコメント出来ます。