7. 変換とパターン
前章では全ての図形が原点にあり、別の場所に置く時は p から位置を引きましたが、其の理由はちゃんと説明しませんでした。
此の章では其れを説明し、更に先に進みます。
図形を移動・回転・拡大縮小し、時間でアニメーションさせ、1行でも何百回も繰り返します。
グリッド、市松模様、レンガの壁、鏡像、放射状のパターンです。
其の全ての裏にあるのは、線型代数シリーズの一つの考え方です。
フラグメントシェーダーは図形を動かさず、代わりに座標を動かします。
そして、座標を動かすのは図形を動かす事の逆です。
章の最後では、全てを組み合わせて、レンガの壁に掛かった動く掛け時計を作ります。
第6章と同じく、変更するのは shader/playground.glsl だけです。
第6章の関数(sdCircle、sdBox、sdSegment、sdRoundBox、fill、smin、paint)は毎回繰り返さずに使います。
7.1 図形を動かす事は座標を動かす事
フラグメントシェーダーが実際に何をしているか考えてみましょう。
ピクセル毎に1回実行され、ピクセルの位置を受け取り、距離関数に「此の点は図形からどれ位離れているか?」と聞きます。
sdCircle(p, r) が知っている円は一つだけ、原点を中心とする円です。
では、中心がの円が欲しいとします。
にあるピクセルにとって「にある円からどれ位離れているか?」という質問は、「は原点にある円からどれ位離れているか?」と同じ質問です。
ピクセルの位置を図形自身の中心から見た位置で表し、其れを元の関数に聞くのです:
つまり、図形を動かすには、座標を動かします。
此れが一般的な規則です。
図形に適用したい変換がなら、距離関数をで評価します。
逆変換を使うのです(線型代数の第10章)。
「だけ動かす」の逆は「だけ動かす」で、其の為に第6章では何処でも p - c と書いていました。
向きがはっきり分かる様に、2つの箱で作ったL字形を使います。
此れなら上下左右が区別出来ます:
// L字形(向きが分かる様に)
float sdL(vec2 p) {
return min(sdBox(p - vec2(0.0, 0.1), vec2(0.05, 0.25)),
sdBox(p - vec2(0.15, -0.1), vec2(0.2, 0.05)));
}
void main() {
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
vec3 color = vec3(0.10, 0.10, 0.12);
vec2 c = vec2(0.6, 0.3); // 図形を動かしたい場所
float d0 = sdL(p); // 元の位置
float d = sdL(p - c); // 座標から c を引く → 図形は +c に動く
color = mix(color, vec3(0.45), outline(d0));
color = mix(color, vec3(1.00, 0.75, 0.20), fill(d));
out_color = vec4(color, 1.0);
}
outline は第6章6.9のヘルパーです:1.0 - smoothstep(0.0, 1.5 * fwidth(d), abs(d))。
c を時間に依存させると図形が動きます。
vec2 c = 0.5 * vec2(cos(uTime), sin(uTime)); なら、秒毎に半径0.5の円を1周します。
7.2 回転
線型代数の第8章の回転行列は、点を角度だけ反時計回りに回します:
7.2.1 mat2を作る
GLSLには2×2行列の型 mat2 があります。
其のコンストラクタは4つの数を行毎ではなく列毎に受け取ります(第2章で見た通り、GLSLの行列は列優先です)。
の1列目は、2列目はなので:
// 回転行列(角度aはラジアン、反時計回り)。mat2は列毎に値を並べる
mat2 rot(float a) {
float c = cos(a), s = sin(a);
return mat2(c, s, -s, c); // 1列目 (c, s)、2列目 (-s, c)
}
紙に書いた見た目通りに mat2(c, -s, s, c) と書くと転置になり、逆向きに回ります。
此れはGLSLで最もよくあるバグの一つで、結果がやはり回転なので気付き難いです。
M * v は本物の行列・ベクトル積です(線型代数の第6章)。
(ラジアン)、つまり、で、に適用して確かめましょう:
x軸上の点がy軸に向きます。
反時計回りに4分の1回転です。
コードでは、rot(1.5708) * vec2(1.0, 0.0) は(丸め誤差を除いて)vec2(0.0, 1.0) になります。
7.2.2 座標を回すと図形は逆に回る
ここでも逆の規則です。
rot(a) * p は座標を反時計回りに回すので、図形は時計回りに回ります。
図形を a だけ反時計回りに回すには、逆回転の rot(-a) を使います:
float a = 0.5236; // 30°
vec2 pl = p - vec2(-0.6, 0.0);
vec2 pr = p - vec2(0.6, 0.0);
float d0 = min(sdL(pl), sdL(pr)); // 回転前
float d1 = sdL(rot(a) * pl); // 左:rot(a)を座標に → 時計回り
float d2 = sdL(rot(-a) * pr); // 右:rot(-a)を座標に → 反時計回り
シェーダーのコードでは p * rot(a) もよく見掛けます。
GLSLでは、行列の左にあるベクトルは行ベクトルとして扱われ、v * M は transpose(M) * v と同じです。
回転では転置が逆行列なので(線型代数の第10章)、p * rot(a) は rot(-a) * p と同じです。
どちらでも構いませんが、あたしは意味が分かり易い rot(-a) * p と書きます。
回転は常に、与えた座標の原点の周りで回ります。
図形を其の中心の周りで回すには、先ず図形が自分の座標の原点に来ていなければなりません。
其処で変換の組み合わせ(7.4)が必要になります。
7.3 拡大縮小
図形を倍に大きくするには、逆の規則に従って座標をで割ります。
但し、回転や移動には無かった落とし穴があります。
半径0.2の円を倍にすると、半径0.4の円になる筈です。
点では:
境界は正しい場所にありますが、距離は小さい図形の単位で測られています。
拡大した世界では全ての距離が倍になるので、掛けて戻します:
問題になるのでしょうか?
fill ヘルパーで普通に塗るだけなら、殆どなりません。
fwidth が d の1ピクセル毎の本当の変化を測るので、どちらでも境界は1ピクセル幅のままです。
然し、距離を長さとして使う物は全て壊れます。
輪郭、角丸、光彩、影です。
拡大した円に太さ0.04のリング(abs(d) - 0.02)を描き、補正無しと補正有りを比べてみましょう:
float s = 2.0;
vec2 pl = p - vec2(-0.65, 0.0);
vec2 pr = p - vec2(0.65, 0.0);
float dl = abs(sdCircle(pl / s, 0.2)) - 0.02; // 左:距離を直さない → 線も2倍太い
float dr = abs(sdCircle(pr / s, 0.2) * s) - 0.02; // 右:距離に s を掛けて直す
非一様な拡大縮小(xとyで違う倍率、p / vec2(sx, sy))は円を楕円にしますが、掛けて戻す一つの倍率がありません。
方向によって距離の伸び方が違うからです。
min(sx, sy) を掛けると決して大き過ぎない距離になり、2Dの描画なら大抵其れで十分です。
楕円の正確な距離は意外と難しいので、殆どの用途では近似で問題ありません。
7.4 変換の組み合わせ:順番が大事
線型代数では変換をの様な積で書きました。
先に回転し、次に平行移動します(第7章と第9章)。
図形を描くには其の逆が必要で、積の逆は逆行列の積を逆の順番にした物です:
右から左に読みます。
先ず平行移動を元に戻し(を引く)、次に回転を元に戻します(だけ回す)。
コードでは rot(-a) * (p - c) です。
図形はの周りでその場で回ります。
逆の順番(先に図形を原点から離し、次に全体を原点の周りで回す)の逆は、つまり rot(-a) * p - c です。
今度は図形が、軌道上の惑星の様に原点の周りを円を描いて回ります。
次のシェーダーは両方を45°ずつ8回描き、後の物程明るくします:
vec2 c = vec2(0.35, 0.0);
vec2 pl = p - vec2(-0.75, 0.0);
vec2 pr = p - vec2(0.6, 0.0);
// 8つの角度で描き、新しい物程明るくする
for (int i = 0; i < 8; i++) {
float a = float(i) * 0.7854; // 45°ずつ
float k = 0.3 + 0.7 * float(i) / 7.0;
float dl = sdL(rot(-a) * (pl - c) * 2.0) / 2.0; // 左:回転してから移動 → その場で回る
float dr = sdL((rot(-a) * pr - c) * 2.0) / 2.0; // 右:移動してから回転 → 原点の周りを回る
color = mix(color, vec3(0.50, 0.75, 1.00) * k, fill(dl));
color = mix(color, vec3(1.00, 0.75, 0.20) * k, fill(dr));
}
// 回転の中心
color = mix(color, vec3(1.0), fill(min(sdCircle(pl - c, 0.02), sdCircle(pr, 0.02))));
* 2.0 と / 2.0 は、7.3の規則でとした半分への縮小です。
座標を0.5で割り(2を掛け)、距離に0.5を掛けます(2で割ります)。
float(i) はループカウンタを float に変換します。
デスクトップのGLSLなら自動で変換されますが(第2章)、変換を書いておけば、決して自動変換しないGLSL ESでもそのまま使えます。
float(i) * 0.7854 を uTime に替えてループを取り除くと、回る図形と公転する図形になります。
一般に、動きを言葉で表す時(「拡大して、回して、其処に動かす」)、コードは逆の手順を逆の順番で適用します。
rot(-a) * (p - c) / s として、最後に距離に s を掛けます。
7.5 繰り返し:floorとfract
ここからが座標の技の本当に強力な所です。
図形を何度も描く代わりに、画面の全ての部分が「自分は原点の近くにいる」と思う様に座標を折り畳みます。
7.5.1 floorとfract
floor(x):より大きくない最大の整数。floor(2.7)= 2、floor(-0.3)= −1。fract(x):小数点以下の部分、。常にの範囲です。fract(2.7)= 0.7、fract(-0.3)= 0.7(だから)。
どちらもベクトルに対して成分毎に動作します。
従って、点 q に対して floor(q) は其の点が入っているグリッドのマスの番号、fract(q) はマスの中での位置です。
7.5.2 図形のグリッド
1単位にマス入る様に座標を倍し、マスの番号とマスの中の位置に分け、マスの中の位置を中央に寄せます(0.5を引くと−0.5〜0.5になります):
float n = 4.0; // 1単位に4マス(画面の縦は8マス)
vec2 q = p * n; // 1マス = 1単位
vec2 id = floor(q); // マスの番号(整数)
vec2 f = fract(q) - 0.5; // マスの中の座標(-0.5〜0.5)
での計算例:
此の点はマス(1, −1)の、左下の角寄りにあります。
全てのマスに中心が原点の小さな座標系が出来たので、sdCircle(f, r) で全てのマスに一度に円が描けます。
距離はマスの単位なので、拡大縮小の規則に従ってで割り、p の単位に戻します。
マスの番号 id はマスの中の全てのピクセルで同じなので、其れを使ってマス毎に色と大きさを変えられます:
// fwidthを使わない塗り(fractの継ぎ目では隣のピクセルとの差が意味を持たない為)
float fillPx(float d) {
float px = 2.0 / uResolution.y;
return 1.0 - smoothstep(-px, px, d);
}
void main() {
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
vec3 color = vec3(0.10, 0.10, 0.12);
float n = 4.0; // 1単位に4マス(画面の縦は8マス)
vec2 q = p * n; // 1マス = 1単位
vec2 id = floor(q); // マスの番号(整数)
vec2 f = fract(q) - 0.5; // マスの中の座標(-0.5〜0.5)
// マスの番号で色と大きさを変える
vec3 cellColor = 0.5 + 0.5 * cos(6.2832 * (id.x * 0.13 + id.y * 0.29) + vec3(0.0, 2.0, 4.0));
float r = 0.25 + 0.15 * sin(id.x * 1.7 + id.y * 2.3);
float d = sdCircle(f, r) / n; // マスの単位からpの単位に戻す
color = mix(color, cellColor, fillPx(d));
// マスの境界線
vec2 g = abs(f);
float grid = 0.5 - max(g.x, g.y);
color = mix(color, vec3(0.3), fillPx(abs(grid / n) - 0.002) * 0.6);
out_color = vec4(color, 1.0);
}
色の式 0.5 + 0.5 * cos(6.2832 * t + vec3(0.0, 2.0, 4.0)) は第5章のプレイグラウンドと同じコサインのパレットです。
t が変わると赤・緑・青のコサインがずれて変わるので、虹の色を巡ります。
id.x と id.y を違う係数で混ぜると、マス毎に違う t になります。
乱数ではありませんが(本当の乱数は第8章)、面白く見える程度には変化があります。
グリッドの線は第6章の箱の技を使っています。
max(g.x, g.y) は「箱」の意味でマスの中心からどれ位離れているかなので、0.5 - max(g.x, g.y) はマスの境界までの距離です。
7.5.3 落とし穴:継ぎ目でのfwidth
何故第6章の fill ではなく fillPx なのでしょうか?
fwidth は値を隣のピクセルと比べます。
二つのマスの境界では、fract が殆ど1から0に跳び、id が変わります。
此の例では円の半径が id に依存するので、距離がマスからマスへ跳び、fwidth が巨大な変化を報告し、fill が境界を何ピクセルにも渡ってぼかします。
結果は、マスの境界に沿った薄い色の線です。
値が跳ぶ所(fract、floor、mod の継ぎ目)では、fillPx が 2.0 / uResolution.y でしている様に、ピクセルの大きさを直接測って下さい。
此れが使えるのは d を p の単位に戻しているからです。
常にで割るべき理由がもう一つ増えました。
繰り返しにはもう一つ制限があります。
各ピクセルは自分のマスしか知りません。
マスからはみ出た図形は境界で切れます。
隣のマスが其れを描かないからです。
図形はマスの中に収めるか、隣のマスも調べて下さい(第8章のボロノイノイズで其れをします)。
時間を使うとパターンがずっと生き生きします。
次の例では、各マスの角丸の四角が rot(-(uTime * 1.5 + phase)) * f で其々の位相で回り、呼吸する様に大きさを変えます(phase = id.x * 0.5 + id.y * 0.9):
7.6 modと市松模様
mod(x, y) はをで割った余りで、次の様に定義されています:
正の数なら普通の余りです。
mod(7.0, 3.0) = 。
負の数ではCの fmod と違います。
GLSLの mod(-1.0, 2.0) はで、常に0との間ですが、Cの fmod(-1.0, 2.0) は−1です。
パターンにはGLSLの振る舞いが丁度良いです。
パターンが負の座標へ継ぎ目無く続くからです。
市松模様はマスの番号の偶奇です。
id.x + id.y が偶数なら暗いマス、奇数なら明るいマスです。
void main() {
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
vec2 id = floor(p * 4.0);
float checker = mod(id.x + id.y, 2.0); // 0, 1, 0, 1, ...
vec3 color = mix(vec3(0.15, 0.15, 0.18), vec3(0.85, 0.80, 0.70), checker);
out_color = vec4(color, 1.0);
}
座標の符号が変わるウィンドウの中央でも、パターンが滑らかに続いている事に注目して下さい。
Cの fmod だと、中央より左と下のマスの色が間違ってしまいます。
7.7 レンガ
レンガの壁は、1段おきにレンガ半分ずれたグリッドです。
先に段を求め、奇数段のxをずらしてから、マスに分けます:
void main() {
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
vec2 size = vec2(0.4, 0.2); // レンガ1つの大きさ
vec2 q = p / size; // 1レンガ = 1単位
float row = floor(q.y);
q.x += 0.5 * mod(row, 2.0); // 1段おきに半分ずらす
vec2 id = floor(q);
vec2 f = (fract(q) - 0.5) * size; // レンガの中の座標(pの単位)
float d = sdRoundBox(f, size * 0.5 - 0.012, 0.02); // 目地の分だけ小さく
float shade = 0.85 + 0.15 * cos(id.x * 2.7 + id.y * 5.1); // レンガ毎の明るさ
vec3 brick = vec3(0.70, 0.32, 0.22) * shade;
vec3 mortar = vec3(0.55, 0.53, 0.50);
vec3 color = mix(mortar, brick, fill(d));
out_color = vec4(color, 1.0);
}
ここではマスが正方形ではないので、後で距離を割る代わりに、マスの中の位置に size を掛けて戻し、p の単位で直接レンガを描いています。
各レンガはマスより少し小さい角丸の箱で、隙間が目地です。
全てのレンガが同じ大きさなので距離は継ぎ目でも連続しており、ここでは普通の fill が使えます。
7.8 absで鏡映対称
abs は空間を折り畳みます。
vec2(abs(p.x), p.y) は画面の左半分を右半分に写すので、右に描いた物が左に鏡映されて現れます。
abs(p) は両方の軸を折り畳み、4つのコピーになります。
此れは既に sdBox の中で使いました。
4分の1に折り畳んだお陰で、角を一つだけ扱えば良かったのです。
float sdPetal(vec2 p) {
float d = sdSegment(p, vec2(0.15, 0.1), vec2(0.6, 0.5)) - 0.07;
return min(d, sdCircle(p - vec2(0.75, 0.2), 0.12));
}
void main() {
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
vec3 color = vec3(0.10, 0.10, 0.12);
float d0 = sdPetal(p); // 元の図形
float d = sdPetal(vec2(abs(p.x), p.y)); // xを鏡映
color = mix(color, vec3(1.00, 0.75, 0.20), fill(d));
color = mix(color, vec3(0.95, 0.35, 0.35), fill(abs(d0) - 0.006));
out_color = vec4(color, 1.0);
}
左の画像は vec2(abs(p.x), p.y)、右は abs(p) です。
顔、ロゴ、雪の結晶、装飾は対称性に満ちていて、折り畳む度に作業が半分になります。
7.9 極座標
グリッドはxとyに沿って繰り返します。
中心の周りに繰り返す物(花、車輪、時計の文字盤、レーダー画面)では、代わりに中心からの距離と角度で点を表します:
GLSLの atan には2つの形があります。
引数2つの atan(y, x)(Cでは atan2)は、正のx軸から反時計回りに測った−πからπまでの完全な角度を返します。
未定義なのは両方の引数が0の時だけです。
引数1つの atan(t) はからまでの角度を返し、反対の方向を区別出来ないので、極座標には必ず引数2つの形を使って下さい。
角度を色に、半径を縞にして表示してみます:
void main() {
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
float r = length(p); // 中心からの距離
float a = atan(p.y, p.x); // 角度(-π〜π)
vec3 color = 0.5 + 0.5 * cos(a + vec3(0.0, 2.0, 4.0)); // 角度で色相
color *= 0.75 + 0.25 * cos(62.83 * r); // 半径0.1毎の縞
out_color = vec4(color, 1.0);
}
中心の左側で角度はπから−πに跳びますが、其処で色に継ぎ目はありません。
コサインの周期がだからです。
此れは覚えておいて下さい。
から計算する物は全て周期であるべきで、そうでないと負のx軸に沿って継ぎ目が出ます。
7.9.1 花
円の半径は一定です。
半径を角度に依存させると、円から花びらが生えます:
なら、半径が円を1周する間に5回上下します。
整数ならは周期なので、継ぎ目はありません。
// 花:半径が角度で変わる円(正確な距離ではないので、小さめの係数を掛ける)
float sdFlower(vec2 p, float petals, float r, float amp) {
float a = atan(p.y, p.x);
return (length(p) - (r + amp * cos(petals * a))) * 0.5;
}
此れは正確な距離ではありません。
花びらの間では本当の距離より速く値が変わり、此の大きさでは約2倍速くなります。
0.5を掛けると大体正しい尺度に戻るので、輪郭や光彩が大体正しい幅になります(第14章のレイマーチングでは、大き過ぎる距離はレイの行き過ぎの原因になります)。
fill には関係ありません。
fwidth が関数の傾きを其のまま測るからです。
7.9.2 角度の繰り返し
fract は直線に沿って繰り返します。
円の周りに繰り返すには、1周を個の等しい区画に分け、点がどの区画にあるかを求め、最初の区画に回して戻します:
floor の前にを足すと一番近い区画に丸められるので、区画0はx軸から始まるのではなく、x軸が中心になります。
(30°の区画)で67°にある点での計算例:なのでとなり、回すと点は7°、最初の目盛りの直ぐ隣に来ます。
全てのピクセルが区画0の近くに来るので、x軸上の sdBox 1つで12個の目盛り全てが描けます:
// 右:角度の繰り返しで12個の目盛り
vec2 q = p - vec2(0.65, 0.0);
float sector = 6.2832 / 12.0; // 1区画の角度(30°)
float a = atan(q.y, q.x);
float k = floor(a / sector + 0.5); // 一番近い区画の番号
vec2 r = rot(-k * sector) * q; // 区画の中心をx軸に回す
float tick = sdBox(r - vec2(0.45, 0.0), vec2(0.07, 0.02));
color = mix(color, vec3(1.00, 0.75, 0.20), fill(tick));
左側の花は sdFlower(p - centre, 3.0, 0.24, 0.1)、(5.0, 0.25, 0.08)、(8.0, 0.3, 0.06) で描いています。
7.10 全部まとめる:掛け時計
此の章の最終的なプレイグラウンドは、テスターを起動してからの本物の秒数を表示する時計です。
時計の広告でいつも見る10時10分から始まります。
此の章の殆ど全てを使います。
壁にはレンガ、60 + 12個の目盛りには角度の繰り返し、針には回転、そして角度を小さく保つ為に fract です。
以下が playground.glsl の新しい部分です。
其の上の第6章の関数はそのままで、キャラクターの sdHead だけを削除しています。
// ---- 第7章:座標の変換と繰り返し ----
// 回転行列(角度aはラジアン、反時計回り)。mat2は列毎に値を並べる
mat2 rot(float a) {
float c = cos(a), s = sin(a);
return mat2(c, s, -s, c); // 1列目 (c, s)、2列目 (-s, c)
}
// fwidthを使わない塗り(fractの継ぎ目では隣のピクセルとの差が意味を持たない為)
float fillPx(float d) {
float px = 2.0 / uResolution.y;
return 1.0 - smoothstep(-px, px, d);
}
// レンガの壁(第7章のタイル)
vec3 bricks(vec2 p) {
vec2 size = vec2(0.4, 0.2);
vec2 q = p / size;
q.x += 0.5 * mod(floor(q.y), 2.0); // 1段おきに半分ずらす
vec2 id = floor(q);
vec2 f = (fract(q) - 0.5) * size;
float d = sdRoundBox(f, size * 0.5 - 0.012, 0.02);
float shade = 0.85 + 0.15 * cos(id.x * 2.7 + id.y * 5.1);
return mix(vec3(0.55, 0.53, 0.50), vec3(0.70, 0.32, 0.22) * shade, fillPx(d));
}
// 角度の繰り返し:pを一番近い区画の中心(x軸上)に回す
vec2 repeatAngle(vec2 p, float count) {
float sector = 6.2832 / count;
float k = floor(atan(p.y, p.x) / sector + 0.5);
return rot(-k * sector) * p;
}
// 針:12時の方向を向いたカプセルを、時計回りに回す
// 座標をrot(+a)で回すと、図形は-a、つまり時計回りに回る
float sdHand(vec2 p, float turn, float back, float len, float width) {
vec2 q = rot(turn * 6.2832) * p;
return sdSegment(q, vec2(0.0, -back), vec2(0.0, len)) - width;
}
// 時針と分針を1つの距離に(影にも使う)
float sdHands(vec2 p, float t) {
float h = sdHand(p, fract(t / 43200.0), 0.08, 0.36, 0.028); // 時針:12時間で1周
float m = sdHand(p, fract(t / 3600.0), 0.10, 0.54, 0.018); // 分針:1時間で1周
return min(h, m);
}
void main() {
// 中心が原点、縦が-1〜1になる座標(第5章)
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
// 10時10分0秒から始まる時刻(秒)
float t = uTime + 10.0 * 3600.0 + 10.0 * 60.0;
// 背景:少し暗くしたレンガの壁
vec3 color = bricks(p) * 0.55;
// 時計の影
float shadow = sdCircle(p - vec2(0.04, -0.06), 0.8);
color *= 1.0 - 0.6 * (1.0 - smoothstep(-0.05, 0.1, shadow));
// 枠と文字盤
float rim = sdCircle(p, 0.8);
float face = sdCircle(p, 0.72);
color = paint(color, vec3(0.20, 0.18, 0.22), rim);
color = paint(color, vec3(0.96, 0.93, 0.85), face);
// 目盛り:60個の小さい物と12個の大きい物
float minuteTick = sdBox(repeatAngle(p, 60.0) - vec2(0.65, 0.0), vec2(0.025, 0.004));
float hourTick = sdBox(repeatAngle(p, 12.0) - vec2(0.62, 0.0), vec2(0.06, 0.014));
color = paint(color, vec3(0.25), minuteTick);
color = paint(color, vec3(0.12), hourTick);
// 針の影、時針と分針
color *= 1.0 - 0.3 * (1.0 - smoothstep(-0.02, 0.03, sdHands(p - vec2(0.015, -0.025), t)));
color = paint(color, vec3(0.12, 0.12, 0.15), sdHands(p, t));
// 秒針:1秒毎にカチッと動く
float second = sdHand(p, fract(floor(t) / 60.0), 0.15, 0.6, 0.006);
second = min(second, sdCircle(p, 0.035));
color = paint(color, vec3(0.85, 0.15, 0.15), second);
out_color = vec4(color, 1.0);
}
面白い所:
- 時計回りの針。 此方の回転は反時計回りですが、時計は逆に回ります。逆の規則がタダで解決してくれます。座標に
rot(+a)を使うと図形は、つまり時計回りに回ります。各針は上(12時)を向いたカプセルで、turnは1周の割合です。0.25は3時、0.5は6時です。 - 割合。 時針は12時間 = 43200秒で1周するので割合は、分針は3600秒で1周します。開始時はで、周は文字盤の10.17時、の小数部分周は10分です。10時10分です。
- 何故fractか。
floatの有効桁数は約7桁です。巨大な角度のcosとsinは精度を失い、テスターをしばらく動かしていると針が震え始めます。fractは1周の割合を0〜1に保つので、角度は決してを超えません。精度はシェーダーで繰り返し出て来る問題で、第8章でもう一度痛い目に遭います。 - カチッと動く秒針。
floor(t)は1秒に1回しか変わらないので、針は本物のクォーツ時計の様に跳びます。floorを外すと滑らかに動く針になります。 - 影。 第6章のキャラクターと同じです。同じ距離をずらし、広い
smoothstepでぼかします。 - 目盛り。
repeatAngle(p, 60.0)とrepeatAngle(p, 12.0)で、60個と12個の箱が其々sdBox1回の呼び出しになります。区画の境界では両側の目盛りまでの距離が同じなので距離は連続しており、fillが使えます。
7.11 此の章の新しいGLSLの機能
| 機能 | 何をするか |
|---|---|
mat2(a, b, c, d) |
1列目が(a, b)、2列目が(c, d)の2×2行列 |
M * v |
行列と列ベクトルの積 |
v * M |
行ベクトルと行列の積、transpose(M) * v と同じ |
floor(x) |
xより大きくない最大の整数 |
fract(x) |
、常に[0, 1) |
mod(x, y) |
、負のxでも0とyの間 |
atan(y, x) |
点(x, y)の角度、−πからπ |
atan(t) |
tのアークタンジェント、−π/2からπ/2 |
float(i) |
intをfloatに変換 |
7.12 実際のゲームやエンジンでの使い方
- テクスチャのタイリングとスクロール。
fractの技は、テクスチャのハードウェアがGL_REPEATラップモード(第11章)でしている事其の物です。床のタイル、草のテクスチャ、レンガのマテリアルはテクスチャ座標を掛け算して繰り返します。其処にuTimeを足すとテクスチャがスクロールし、多くのゲームはベルトコンベア、滝、溶岩を此れでアニメーションさせています。 - プロシージャルマテリアル。 マテリアルエディタ(Unrealのマテリアルグラフ、Blenderのシェーダーノード、Substance Designer)のレンガ、タイル、板、市松模様は、此れと同じノードで作られています。floor、fract、mod、マス毎の図形、そして変化を付ける為のマスの番号です。
- 円形のUI。 アビリティアイコンのクールダウン表示(MMOやMOBAの回転する影)、円形の体力・スタミナゲージ、ラジアルメニュー、ローディングスピナーは
atan(y, x)と塗りの割合を比べています。ミニマップは正に此のrotで座標を回しています。 - スプライトとパーティクル。 2Dエンジンは同じ2×2行列でスプライトを回転・拡大縮小し、大抵は頂点シェーダー(第13章)で行います。パーティクルシステムはパーティクル毎の角度で各パーティクルを回します。
- 万華鏡と対称性。 鏡映と角度の繰り返しは、万華鏡のポストエフェクト、RPGの魔法陣や呪文のエフェクトに使われます。
- 3D。 レイマーチング(第14章)では、3D座標に
modを使うと、1つ分のコストで物体を全方向に無限に繰り返せます。無限の廊下や柱の森を作る、デモシーンの古典的な技です。
7.13 まとめ
- シェーダーは図形ではなく座標を動かします。図形をで変換するには、距離をで評価します。
- 平行移動:
p - c。回転:mat2(c, s, -s, c)(列毎!)でrot(-a) * p。拡大縮小:f(p / s) * s。 - 変換の組み合わせでは逆の手順を逆の順番で適用します。
rot(-a) * (p - c)はその場で回り、rot(-a) * p - cは公転します。 floorはマスの番号、fractはマスの中の位置を返します。番号はマス毎の変化に使います。距離は倍率で割ります。- 値が跳ぶ継ぎ目では
fwidthが失敗します。其処ではピクセルの大きさ2.0 / uResolution.yを使います。 modで市松模様や交互の段が作れます。Cのfmodと違い、割る数が正なら決して負になりません。absは鏡映、atan(y, x)とlengthは極座標、一番近い区画への回転で図形を円形に繰り返せます。- 精度を失わない様に、角度は
fractで小さく保ちます。
7.14 課題
- ★ 、、について
floor、fract、mod(x, 3.0)を計算して下さい。次に、でのマスの番号と、中央に寄せた位置fract(q) - 0.5を計算して下さい。 - ★ 行列
mat2(0.0, -1.0, 1.0, 0.0)を行と列で書き出し、とに掛けて下さい。どんな回転ですか?座標に適用すると、図形は時計回りと反時計回りのどちらに回りますか? - ★★ L字形を、4秒で中心の周りを1周し、同時に其の倍の速さで自分の角の周りを回る様にアニメーションさせて下さい。
rot2回と引き算1回を正しい順番で使う筈です。2秒後にLが中心の反対側にあり、丁度1回転している事を確かめて下さい。 - ★★ ハニカムの様な模様を描いて下さい。1段おきにマス半分ずれ(レンガの様に)、段の間隔がマスの円のグリッドです。円は6つの隣の円と接する筈です。
- ★★ ローディングスピナーを作って下さい。角度の繰り返しで円上に8個の点を置き、各点の明るさを区画の番号
kとuTimeに依存させて、明るい点が尾を引きながら1秒で1周する様にします。 - ★★ 12時、3時、6時、9時の目盛りを2倍の長さの赤にして下さい。
repeatAngleを、区画の番号kもout float引数(第2章)で返す様に変え、mod(k, 3.0)で其の4つの区画を選びます。注意:atanでは区画0は12時ではなく3時にあります。 - ★★★ 一部の駅の時計の様に、秒針を滑らかに動かしつつ、毎秒少しだけ「弾む」様にして下さい。
fract(t)とsmoothstepか小さな行き過ぎのカーブを使い、毎秒の最初の0.2秒で次の秒に動かします。何故針の回転には相変わらずfract(... / 60.0)を使う必要があるのか説明して下さい。
ファイル: a/shader/playground.glsl | b/shader/playground.glsl
| 前 | 新 | ||
|---|---|---|---|
| @@ -45,59 +45,92 @@ float smin(float a, float b, float k) { | |||
| 45 | return mix(b, a, h) - k * h * (1.0 - h); | 45 | return mix(b, a, h) - k * h * (1.0 - h); |
| 46 | } | 46 | } |
| 47 | 47 | ||
| 48 | // 頭:円に2つの耳を滑らかにくっつける | ||
| 49 | float sdHead(vec2 p) { | ||
| 50 | float head = sdCircle(p, 0.6); | ||
| 51 | float earL = sdCircle(p - vec2(-0.45, 0.5), 0.17); | ||
| 52 | float earR = sdCircle(p - vec2(0.45, 0.5), 0.17); | ||
| 53 | return smin(head, min(earL, earR), 0.12); | ||
| 54 | } | ||
| 55 | |||
| 56 | // 図形を色で塗り重ねる | 48 | // 図形を色で塗り重ねる |
| 57 | vec3 paint(vec3 base, vec3 ink, float d) { | 49 | vec3 paint(vec3 base, vec3 ink, float d) { |
| 58 | return mix(base, ink, fill(d)); | 50 | return mix(base, ink, fill(d)); |
| 59 | } | 51 | } |
| 60 | 52 | ||
| 53 | // ---- 第7章:座標の変換と繰り返し ---- | ||
| 54 | |||
| 55 | // 回転行列(角度aはラジアン、反時計回り)。mat2は列毎に値を並べる | ||
| 56 | mat2 rot(float a) { | ||
| 57 | float c = cos(a), s = sin(a); | ||
| 58 | return mat2(c, s, -s, c); // 1列目 (c, s)、2列目 (-s, c) | ||
| 59 | } | ||
| 60 | |||
| 61 | // fwidthを使わない塗り(fractの継ぎ目では隣のピクセルとの差が意味を持たない為) | ||
| 62 | float fillPx(float d) { | ||
| 63 | float px = 2.0 / uResolution.y; | ||
| 64 | return 1.0 - smoothstep(-px, px, d); | ||
| 65 | } | ||
| 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軸上)に回す | ||
| 80 | vec2 repeatAngle(vec2 p, float count) { | ||
| 81 | float sector = 6.2832 / count; | ||
| 82 | float k = floor(atan(p.y, p.x) / sector + 0.5); | ||
| 83 | return rot(-k * sector) * p; | ||
| 84 | } | ||
| 85 | |||
| 86 | // 針:12時の方向を向いたカプセルを、時計回りに回す | ||
| 87 | // 座標をrot(+a)で回すと、図形は-a、つまり時計回りに回る | ||
| 88 | float sdHand(vec2 p, float turn, float back, float len, float width) { | ||
| 89 | vec2 q = rot(turn * 6.2832) * p; | ||
| 90 | return sdSegment(q, vec2(0.0, -back), vec2(0.0, len)) - width; | ||
| 91 | } | ||
| 92 | |||
| 93 | // 時針と分針を1つの距離に(影にも使う) | ||
| 94 | float sdHands(vec2 p, float t) { | ||
| 95 | float h = sdHand(p, fract(t / 43200.0), 0.08, 0.36, 0.028); // 時針:12時間で1周 | ||
| 96 | float m = sdHand(p, fract(t / 3600.0), 0.10, 0.54, 0.018); // 分針:1時間で1周 | ||
| 97 | return min(h, m); | ||
| 98 | } | ||
| 99 | |||
| 61 | void main() { | 100 | void main() { |
| 62 | // 中心が原点、縦が-1〜1になる座標(第5章) | 101 | // 中心が原点、縦が-1〜1になる座標(第5章) |
| 63 | vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y; | 102 | vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y; |
| 64 | vec2 m = (2.0 * uMouse - uResolution) / uResolution.y; // マウスも同じ座標に | 103 | |
| 65 | 104 | // 10時10分0秒から始まる時刻(秒) | |
| 66 | // 背景:縦のグラデーション | 105 | float t = uTime + 10.0 * 3600.0 + 10.0 * 60.0; |
| 67 | vec3 color = mix(vec3(0.16, 0.12, 0.22), vec3(0.08, 0.06, 0.10), p.y * 0.5 + 0.5); | 106 | |
| 68 | 107 | // 背景:少し暗くしたレンガの壁 | |
| 69 | // 影:同じ距離関数をずらして、ぼかして使う | 108 | vec3 color = bricks(p) * 0.55; |
| 70 | float shadow = sdHead(p - vec2(0.05, -0.06)); | 109 | |
| 71 | color *= 1.0 - 0.5 * (1.0 - smoothstep(-0.05, 0.12, shadow)); | 110 | // 時計の影 |
| 72 | 111 | float shadow = sdCircle(p - vec2(0.04, -0.06), 0.8); | |
| 73 | // 頭の塗りと輪郭 | 112 | color *= 1.0 - 0.6 * (1.0 - smoothstep(-0.05, 0.1, shadow)); |
| 74 | float head = sdHead(p); | 113 | |
| 75 | color = paint(color, vec3(1.00, 0.80, 0.30), head); | 114 | // 枠と文字盤 |
| 76 | color = paint(color, vec3(0.35, 0.18, 0.10), abs(head) - 0.012); | 115 | float rim = sdCircle(p, 0.8); |
| 77 | 116 | float face = sdCircle(p, 0.72); | |
| 78 | // ほっぺ:境界を広くぼかした円 | 117 | color = paint(color, vec3(0.20, 0.18, 0.22), rim); |
| 79 | float cheek = min(sdCircle(p - vec2(-0.36, -0.12), 0.09), | 118 | color = paint(color, vec3(0.96, 0.93, 0.85), face); |
| 80 | sdCircle(p - vec2(0.36, -0.12), 0.09)); | 119 | |
| 81 | color = mix(color, vec3(1.0, 0.50, 0.50), 0.6 * (1.0 - smoothstep(-0.04, 0.04, cheek))); | 120 | // 目盛り:60個の小さい物と12個の大きい物 |
| 82 | 121 | float minuteTick = sdBox(repeatAngle(p, 60.0) - vec2(0.65, 0.0), vec2(0.025, 0.004)); | |
| 83 | // 目:縦のカプセル | 122 | float hourTick = sdBox(repeatAngle(p, 12.0) - vec2(0.62, 0.0), vec2(0.06, 0.014)); |
| 84 | vec2 eyeL = vec2(-0.2, 0.12); | 123 | color = paint(color, vec3(0.25), minuteTick); |
| 85 | vec2 eyeR = vec2(0.2, 0.12); | 124 | color = paint(color, vec3(0.12), hourTick); |
| 86 | float eyes = min(sdSegment(p, eyeL - vec2(0.0, 0.06), eyeL + vec2(0.0, 0.06)), | 125 | |
| 87 | sdSegment(p, eyeR - vec2(0.0, 0.06), eyeR + vec2(0.0, 0.06))) - 0.075; | 126 | // 針の影、時針と分針 |
| 88 | color = paint(color, vec3(1.0), eyes); | 127 | color *= 1.0 - 0.3 * (1.0 - smoothstep(-0.02, 0.03, sdHands(p - vec2(0.015, -0.025), t))); |
| 89 | color = paint(color, vec3(0.35, 0.18, 0.10), abs(eyes) - 0.01); | 128 | color = paint(color, vec3(0.12, 0.12, 0.15), sdHands(p, t)); |
| 90 | 129 | ||
| 91 | // 瞳:マウスの方を見る(目の中から出ない様に長さを制限) | 130 | // 秒針:1秒毎にカチッと動く |
| 92 | vec2 look = m - (eyeL + eyeR) * 0.5; | 131 | float second = sdHand(p, fract(floor(t) / 60.0), 0.15, 0.6, 0.006); |
| 93 | look = look / max(length(look), 1.0) * 0.05; | 132 | second = min(second, sdCircle(p, 0.035)); |
| 94 | float pupils = min(sdCircle(p - eyeL - look, 0.04), sdCircle(p - eyeR - look, 0.04)); | 133 | color = paint(color, vec3(0.85, 0.15, 0.15), second); |
| 95 | color = paint(color, vec3(0.12, 0.08, 0.06), pupils); | ||
| 96 | |||
| 97 | // 口:リングの下半分(リングと半平面の積集合) | ||
| 98 | float ring = abs(sdCircle(p - vec2(0.0, 0.02), 0.3)) - 0.022; | ||
| 99 | float below = p.y + 0.12; // y < -0.12 の部分が負 | ||
| 100 | color = paint(color, vec3(0.35, 0.18, 0.10), max(ring, below)); | ||
| 101 | 134 | ||
| 102 | out_color = vec4(color, 1.0); | 135 | out_color = vec4(color, 1.0); |
| 103 | } | 136 | } |
コメント(0)
まだコメントはありません。
ログインするとコメント出来ます。