Skip to content

Instantly share code, notes, and snippets.

@showa-yojyo
Last active December 10, 2021 16:12
Show Gist options
  • Save showa-yojyo/9ca41224f478fa5ffd8374da57d30e89 to your computer and use it in GitHub Desktop.
Save showa-yojyo/9ca41224f478fa5ffd8374da57d30e89 to your computer and use it in GitHub Desktop.
The Book of Shaders 学習ノート Pt. 1
title
The Book of Shaders 学習ノート

The Book of Shaders 学習ノート。 GLSL の、特に断片シェーダーのプログラミングに関する教科書として読む。

About this book

いろいろと書いてあるが、付録との絡みにしか興味がない。 付録を読むときにこの章を読み返せばいいだろう。

Getting started

What is a fragment shader?

この節は今や読み飛ばせる。次のようなことが述べられている:

計算機で絵を描くという処理は、手書きの手紙や本を書くのと似ていて、次から次へと作業を行う命令の集合体だ。 シェーダーも命令の集合体であり、その命令は画面上のすべての画素に対して一度に実行される。 つまり、コードは画面上の画素の位置に応じて異なる動作をしなければならない。 活字印刷機のように、位置を受け取って色を返す関数としてプログラムを動作させ、 それをコンパイルすると超高速で動作することになる。

Why are shaders fast?

この節の記述は面白い。要約すると次のようになる:

画面上の画素一つ一つを単純かつ小さなタスクとみなす。 この小さなタスクは画面上の各画素に対して行われなければならない。 例えば古の 800x600 の画面でさえ、フレームあたり 480,000 画素を処理しなければならず、 秒間 14,400,000回の計算をしなければならないということだ。これをどうクリアしているのか。

並列処理が有効な解決策となる。パイプのたとえで言うと、大きくて強力なパイプをいくつか用意するよりも、 小さな大量のパイプを同時に並列動作させたほうが良い。GPU はこの方針に基づいた処理装置だ。

本書のイラストのように、GPU と各画素データを大量のパイプの束とピンポン玉にそれぞれなぞらえる。

秒間 1,440,000個のピンポン玉を放り込もうとすれば、どんな太いパイプをも塞ぐだろう。 一方、秒間 480,000 画素の波を 30 回受信する 800x600 の小さなパイプからなる束ならばまだ円滑に処理できるだろう。 並列度の高いハードウェアほど大きなストリームを処理することができる。

これとは別に、GPU のもう一つの超能力は、ハードウェアで加速された特殊な数学だ。 あとの章で述べられる複雑な数学演算をソフトウェアではなくマイクロチップが直接解決する。 これにより、三角関数や行列の演算が非常に高速になり、電気の力を最大限に発揮できる。

What is GLSL?

GLSL の定義が書かれている。これは当面の目的には役には立たない。

Why are Shaders famously painful?

シェーダーを書くのは苦行なのかもしれない。

  • 並列処理を行うためには、すべてのスレッド(パイプ)が他のすべてのスレッドから独立していなければならない。 この制約は、すべてのデータが同じ方向に流れなければならないことを意味する。 そのため、他のスレッドの結果を確認したり、入力データを修正したり、 スレッドの結果を他のスレッドに渡したりすることはできない。 スレッド間の通信を許可すると、データの整合性が損なわれる。
  • また、GPU は並列マイクロプロセッサー(パイプ)を多忙にしており、 空き時間になると新しい情報を受け取って処理している。 スレッドが直前の瞬間に何をしていたかを知ることはできない。 それは、OS の UI からボタンを描き、ゲームで空の一部を描き、 それからメールの文章を表示するといったことであったりさえもする。
  • 各スレッドはさらに記憶能力もない。

"Hello world!"

次を技術、知識を習得できる:

  • 断片シェーダーのみで背景をベタ塗りする方法
  • vec4, vec3, float, int, RGBA などの基本事項各種
  • precision {highp,mediump,lowp} float; のような宣言が必要であること
  • GLSL で引数リストが空であるユーザー関数を定義する方法

なお gl_FragColor は WebGL2 などで書く version 300 es なシェーダーではもう使えないはずだ。 普通の out vec4 変数で代替する。


演習問題

  • 浮動小数点数を整数に書き換えるとどうなるか試せ。
  • gl_FragColor への代入式全体をコメントアウトするとどうなるか試せ。
  • 特定の色を返す別の関数を作って、それを main() の中で使ってみろ。

解答

vec4 の実引数を float リテラル値から int リテラル値に書き換えても問題なく動作する。 しかし、他のページのコードではエラーになることがふつうにある。

gl_FragColor への代入をやめると、背景色が地の色のままになる。

GLSL ではユーザー関数を定義することができる。ここではいちばん素朴な実装を示す。

vec4 red(){
    return vec4(1.0, 0.0, 0.0, 1.0);
}

void main() {
    gl_FragColor = red();
}

Uniforms

  • キャンバスサイズ、マウス位置、時刻などを uniform 修飾された変数で処理すること。
  • 先述のように、GLSL がハードウェア加速されている角度、三角関数、指数関数を用意していること。 特に次の関数は全て、この章を読んだ読者が履修したものとみなすようだ:

演習問題

  • 色の変化がほとんど気にならなくなるまで周波数を下げろ。
  • ちらつきのない単一の色が見えるようになるまでスピードを上げろ。
  • RGB チャンネルの周波数をいじれ。面白いパターンや動作が得られる。

解答

周期関数として sin を用いているので、例えば sin(2 * PI * x / T) だと周期は T だから 周波数すなわち 1./T を上げるならば T をゼロに近づける必要がある。

uniform float u_time;

const float tau = radians(360.);
const float T = 1.; // これを書き換えて大きくしたり小さくしたりする

void main() {
    gl_FragColor = vec4(abs(sin(tau * u_time / T)), 0.0, 0.0, 1.0);
}

最後の問いの解答としては、これまでは R チャンネルしか変化させていないが、このようなことを G, B でもすればいい。

gl_FragCoord

  • 定義済み変数 gl_FragCoord の意味。
  • このベクトルをキャンバス解像度で除算すれば、正規化された座標が得られる。
  • キャンバスの座標系の向き。
  • 線形グラデーション塗りの技法。
  • マウス位置、時刻を参照してカラーパターンを変化させる手法。

演習問題

  • 座標 $(0.0, 0.0)$ がキャンバスのどこにあるか。 $(1.0, 0.0), (0.0, 1.0), (0.5, 0.5), (1.0, 1.0)$ はどうか。
  • u_mouse の値が画素単位であり、正規化された値ではないことを理解した上で、 u_mouse の使い方を理解したか。これを使って、色を動かすことができるか。
  • u_timeu_mouse を使って、このカラーパターンを変更する面白い方法を想像できるか。

解答

まず、スクリーンの座標系の向きは数学と同じだから、原点はキャンバスのいちばん左下の点(本書では点とは呼んでいないが)となる。 その次の四点はキャンバス座標空間が正規化されたことを前提として、 それぞれキャンバスの右下、左上、中央、右上に等しい。

マウスの問題は微妙で、変数 st と一緒に用いるには正規化する必要がある。 色を動かすには、例えば:

gl_FragColor = vec4(st.x, step(u_mouse.y / u_resolution.y, st.y), 0.0, 1.0);

しかし、u_mouse が一様変数である以上は正規化をシェーダーの外でやるべきだろう。

最後の問いは省略。

Running your shader

著者が製作した、シェーダーを作成、表示、共有、管理するためのツール群。 これらのツールは Linux, MacOS, Windows, Raspberry Pi, ブラウザー間で一貫して動作し、コードを変更する必要がないとある。

Running your shaders on the browser

  • この本に掲載されているすべてのライブサンプルは、 glslCanvas を使って表示している。 スタンドアロンシェーダーの実行プロセスが驚くほど簡単だ。

  • コンソールから直接シェーダーを実行したいと思う人は glslViewer をチェックするといい。 シェルスクリプトやパイプラインにシェーダーを組み込み、 ImageMagick と同様の方法で使用することができる。

    bash$ glslViewer yourShader.frag yourInputImage.png —w 500 -h 500 -E screenshot,yourOutputImage.png
  • glslEditor というオンラインエディターを著者が用意した。 スタンドアローンの Web アプリケーション版もある。

  • And more...

Running your shaders on your favorite framework

本書で使用する u_XXXX 変数と同じものを使って、いくつかの人気フレームワークにシェーダーを設定する方法の例が示されている。

title
The Book of Shaders 学習ノート

Algorithmic drawing

Shaping functions

個人的にはこの章が本書の神髄であると評価している。シェーダー以外にもつぶしが効く。

  • 一次元の関数を作る方法を理解する。
  • 最初の二つのライブコードでプロットの描画方法がわかる。 なお、線形補間の処理には GLSL 関数 mix を利用できる。
  • 0 から 1 の値を黒から白の間の灰色に線形に対応させる。

演習問題

  • pow に代えて exp, log, sqrt を試せ。

解答

何も工夫しないと定義域と値域の都合で explog ではプロット曲線が現れない。 実引数である st.x に下駄を履かせる。

Step and Smoothstep

これまでの人生で一度も有効活用したことがない関数だ。 これらは断片シェーダーの実装でたいへん活躍する。

  • この節で取り扱う GLSL 関数二つは補間に用いる。 細工しなければ定義域は実数全体、値域は閉区間 ${[0, 1]}$ となる。
  • 関数 step(edge, x): 階段関数。
    • シェーダーコードにおいては条件分岐、たとえば if 文の回避に使うことも多い。
  • 関数 smoothstep(a, b, x): 三次関数。これらの関数を補間に応用する。
    • 例えば smoothstep(a, a + d, x) - smoothstep(a + d, a + 2 * d, x) でベルカーブ。

この段階では関数の合成にまだ踏み込まない。踏み込んだほうが良かった。

Sine and Cosine

値が ${[-1, 1]}$ にある滑らかな関数として利用する。

  • 関数 sin(x) など。三角関数の波長と振幅の調整法。
    • サインカーブの floorceil の和を取ると -1, 1 を取るデジタル信号になる。
  • 関数 abs(x)fract(x) で合成すること。

演習問題

  • sin を計算する前に x に時間 u_time を加えろ。
  • sin を計算する前に xPI を掛けろ。位相が縮小し、各周期が 2 ごとに繰り返されるだろう。
  • 時間 u_timex に乗じてから sin を計算しろ。 位相間の周波数がますます圧縮されていくだろう。
  • sin(x) に 1.0 を加えろ。すべての波が上にずれて、すべての値が 0.0 から 2.0 の間になっているだろう。
  • sin(x) に 2.0 を掛けろ。振幅が倍になる。
  • sin(x) の絶対値 (abs()) を計算しろ。弾んだボールの跡のようになるだろう。
  • sin(x) の結果の小数部分 (fract()) だけを取り出せ。
  • sin(x) の結果の整数の大きい方(ceil()) と小さい方 (floor()) を足すと、 1 と -1 の値のデジタル波になる。

回答

この練習では u_time がすでに大きくなっていて、プロットが読みづらくなっている可能性がある。 そこは適当に工夫する。要点をまとめると:

  • sin の引数に定数を掛けると、波が横方向に伸縮する。引数の絶対値を大きくすると縮む。
  • sin に定数を掛けると、波が縦方向に伸縮する。係数の絶対値を大きくすると伸びる。
  • fract の結果は少々驚く。これのおかげで「小数部分」の定義を明確に習得できた。
  • ceilfloor の加工では、実は離散的に値 $\pm 2$ が生じる。

Some extra useful functions

関数 mod, fract, sign, clamp, min, max を体で覚える。

  • mod(x, y) の定義を言えるようにしておく必要がある。
  • sign(x) は -1, 0, 1 のいずれかを返す。この 0 が便利な場合とそうでない場合がある。
関数呼び出し プロット
mod(x, 0.5) いわば半三角波。連続部分は単調増加。
fract(x) これも半三角波だが上のものの倍の三角形を描く。
ceil(x) 上り階段。${[-1, 1]}$ を通る。
floor(x) 上り階段。${[0, 1]}$ を通る。
sign(x) 左が -1 右が 1 の奇関数。ただし原点は例外。
abs(x) V 字を描く。
clamp(x, 0.0, 1.0) ${[0, 1]}$ だけ上り坂になる。あとは直線。
min(0.0, x) 上り坂から平らに変化。
max(0.0, x) 平らから上り坂に変化。

Advance shaping functions

これら単体でノートのページが丸々埋まるのでまともに取り組むか悩む。

Exercise

単純な数式の関数を習得して損はないだろう。 関数 pow を多用するので関数 abs も併用するようだ。 非負だとわかっている合成関数ならば abs 呼び出しを省略する。

y = 1. - sqrt(abs(x));
y = 1. - abs(x);
y = 1. - x * x;
For your toolbox
  • Grapher (Mac only)
  • Graphtoy: これは良い。ノートにペンでスケッチする手間が省ける。
  • Shadershop: 操作が難しい。

一変数関数プロットツールということなら Matplotlib や SymPy で事足りる。

Colors

  • vec3, vec4 のメンバーのアクセス方法
  • ベクトル成分に対する swizzling と呼ばれるアクセス方法

次のようなスニペットを愛用しているテキストエディターに仕込んでおけとある:

vec3($1, $2, $3)
vec4($1, $2, $3, ${4:1.0})

Mixing color

  • 関数 mix で二つの値をパーセンテージで混合できる。線形内分補間。
  • こういう関数も利用できる:Easing Functions Cheat Sheet

Playing with gradients

  • 関数 mixfloat 型だけでなく、同型の vecN を渡すこともできる。

演習問題

  • ターナーの夕日をイメージしたグラデーションを作成しろ。
  • u_time を使って、日の出と日没の間をアニメーションにしろ。
  • これまでに学んだことを使って虹を作れ。
  • 関数 step を使ってカラフルな旗を作れ。

解答

ターナーの課題は三色使いたい。線形グラデーションをうまく指定する。 水平方向の座標成分についてはどうでもいい。

vec3 skyblue = vec3(.5294, .8078, .9216);
vec3 white = vec3(1., 1., 1.);
vec3 darkorange = vec3(1., 0.59, 0.);

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    float height = fract(st.y * 2.);
    vec3 color = step(-.5, -st.y) * mix(darkorange, white, height)
        + step(.5, st.y) * mix(white, skyblue, height);
    // ...
}

アニメーションのは中間地点を時刻とともに下方向へ移動させる方針で行く。

float sunset = fract(u_time);
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    vec3 color = step(-sunset, -st.y) * mix(
        darkorange, white, fract(st.y) / sunset) + 
        step(sunset, st.y) * mix(
            white, skyblue, mix(st.y - sunset, 1., st.y - sunset));
    // ...

虹は次のようなコードになる。曲線を描くのは頑張ればこの時点でもできるが……。 原色の中間が暗いのがいやならば、さらなる色を定義して補間処理を追加すればいいのだが、 その問題は次の節で解決する。

vec3 red = vec3(1., 0., 0.);
vec3 green = vec3(0., 1., 0.);
vec3 blue = vec3(0., 0., 1.);

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    float N = 3.;
    vec3 color
        = (step(0., st.y) - step(1./N, st.y)) * mix(red, green, st.y * N)
        + (step(1./N, st.y) - step(2./N, st.y)) * mix(green, blue, (st.y - 1./N) * N)
        + (step(2./N, st.y) - step(3./N, st.y)) * mix(blue, red, (st.y - 2./N) * N);
    // ...
}

上のコードの mix 部分を色ベクトルで置き換えるとカラフルな旗になる。

HSB

  • 関数 rgb2hsvhsv2rgb の実装。このコードはよそにも用意されているだろう。
  • rgb = rgb*rgb*(3.0-2.0*rgb) の式は Hermite 補間、すなわち smoothstep に見える。

HSB in polar coordinates


演習問題

  • 待機中マウスカーソルみたいなものを描け。
  • HSB から RGB への変換機能と一緒に shaping 機能を使い、 特定の色相値を拡大し、それ以外の色相値を縮小しろ。
  • RYB 色空間版のカラーピッカーのパレットを描け。
  • Interaction of Color (Josef Albers, 2006) を読み、問題文の下にあるシェーダー例を練習として使え。

解答

マウスカーソルの問題は次の一行を加えれば十分:

color *= step(0.5, radius) - step(0.75, radius);

色相拡縮だが、少しむずかしい。まず条件を少し特殊化したものを考える:

  • 特定の色相 $h$ を 0.5 に固定する。
  • $d$ をゼロに十分近い正数とおき、
    • 拡大する色相を ${[h - d, h + d)}$ に含まれものとする。
    • 縮小する色相を ${[0, d) \cup [1 - d, 1)}$ に含まれるものとして固定する。
  • 拡縮を一次関数によって変換する。

ロジックをスケッチすると次のようになる。初版:

// Map the angle (-PI to PI) to the Hue (from 0 to 1)
// and the Saturation to the radius
float hue = angle / TWO_PI + 0.5;

float d = 0.1;
float x1 = d;
float x2 = 1. - d;
float y1 = (1. - d) * .5;
float y2 = (1. + d) * .5;

if(0. <= hue && hue < x1){
    hue = mix(0., y1, hue/d);
}
else if(x1 <= hue && hue < x2){
    hue = mix(y1, y2, (hue - x1) / (x2 - x1));
}
else{
    hue = mix(y2, 1., (hue - x2) / (1. - x2));
}

color = hsb2rgb(vec3(hue, radius, 1.0));

ここから条件分岐などをシェーダーらしく書き直す:

float pulse(float a, float b, float x){
    return step(a, x) - step(b, x);
}

として関数 pulse を定義すると一連の if 文を次の式に置き換えられる:

hue = pulse(0., x1, hue) * mix(0., y1, hue/d)
    + pulse(x1, x2, hue) * mix(y1, y2, (hue - x1) / (x2 - x1)) 
    + pulse(x2, 1., hue) * mix(y2, 1., (hue - x2) / (1. - x2));

各項をさらに関数化する。

float slope(float x1, float x2, float y1, float y2, float hue){
    return pulse(x1, x2, hue) * mix(y1, y2, (hue - x1) / (x2 - x1));
}

// ...

hue = slope(0., x1, 0., y1, hue)
    + slope(x1, x2, y1, y2, hue)
    + slope(x2, 1., y2, 1., hue);

RYB 問題は上の回答を変形させれば解けるだろう。

Interaction of Color 問題はよくわからない。コードを見て終わりとするしかない。

  • 関数 rect は後ほど登場する。
  • ここでの mixstep の使い方は基本的なので、確実に習得したい。 mix の入れ子から分岐処理が見える。
  • なるほど st.y = 1. - st.y とすれば上下逆になる。
  • 対称な位置にある矩形二つを一度の関数呼び出しで描けるのは面白い。
Note about functions and arguments

GLSL の関数引数リストの in, out, inout について。

Shapes

単純な図形を並列に描く手法を習得する。

Rectangle

  • 関数 step を二次元的に採用して矩形を定義する方法
    • L 型を反復する方法
    • ロ型を反復する方法
vec2 bl = step(vec2(0.1), st); // bottom-left
vec2 tr = step(vec2(0.1), 1.0 - st); // top-right
color = vec3(bl.x * bl.y * tr.x * tr.y);

演習問題

  • 矩形のサイズと比率を変更しろ。
  • step の代わりに smoothstep を使って実験しろ。ぼやけたエッジからエレガントで滑らかな境界線になる。
  • floor を使った別の実装を考えろ。
  • 最も気に入った実装を選び、将来的に再利用できるようにその関数を作れ。関数は柔軟で効率的なものにしろ
  • 矩形の輪郭を描くだけの別の関数を作れ。
  • 同じキャンバスの中で、異なる矩形を移動したり配置したりするにはどうしたらいいか。 方法がわかったらモンドリアンのような平面構成をして自分の技術を誇示しろ。

解答

この問題から、関数 step を使える場合には smoothstep で置き換わるかどうかを考えると良いことがわかる。

次のように floor を使うと同じ結果が得られる:

vec2 bl = floor(0.9 + st);
vec2 tr = floor(1.9 - st);

キャンバス全域を矩形で囲む関数 rectangle を定義する。 柔軟にすることを要求されているが、初版なので色々と決め打ちする。 線幅や矩形のサイズ、位置を引数リストに追加することが考えられる。

float rectangle(vec2 st, float border_width){
    vec2 border = vec2(max(0.01, border_width));
    // bottom-left
    vec2 bl = step(border, st);
    float pct = bl.x * bl.y;

    // top-right
    vec2 tr = step(border, 1.0 - st);
    pct *= tr.x * tr.y;
    return pct;
}

単純塗りつぶしバージョン。以前定義した関数 pulse を用いる。

float rectangle(vec2 st, vec2 bl, vec2 tr){
    return pulse(bl.x, tr.x, st.x) * pulse(bl.y, tr.y, st.y);
}

複数の異なる矩形を描くのは条件を与えれば容易だ:

  • 背景は黒とする。
  • どの矩形も白とする。

これならば上記の rectangle を呼び出した戻り値をその都度 max を取っていけばいい。 矩形に色が着いていたり、相異なる矩形が重なり合うことを考えると問題が難しくなる。

Circles

  • 円を描く方法

演習問題

  • そこから何が推測できるのか。
  • これを使ってどのように円を描くことができるか。
  • 上のコードを修正して、円のグラデーション全体をキャンバス内に収めろ。

解答

当然だが、固定点から距離が一定であるピクセルだけを他の色で描けば円が現れる。

float d = .01;
vec3 color = 1. - vec3(step(.5 - d, pct) - step(.5, pct));

いつものように smooth に代えて smoothstep を使うと描線が滑らかになる。

グラデーション全体をキャンバス内に収めるだけならば次のように修正すれば十分:

vec3 color = vec3(pct) * 2.;

Distance field

距離場の概念:平面から非負実数への写像という解釈でよかろう。


演習問題

  • step を使って、0.5 以上のものを白に、以下のものを黒にしろ。
  • 背景と前景の色を反転させろ。
  • smoothstep を使って、円の境界線が滑らかになるように様々な値を試せ。
  • 満足のいく実装ができたら、将来的に再利用できるように、その関数を用意しろ。
  • 円に色を与えろ。
  • 円が大きくなったり小さくなったりするのをアニメーションで表現して、 心臓の鼓動を真似てみろ。
  • この円を動かすのはどうか。移動させて、キャンバスに異なる円を配置することはできるか。
  • 異なった関数や操作を使って、距離場を組み合わせるとどうなるか。
  • この技法を使って三つの合成を作れ。アニメーションがあればなお良し。

解答

白と黒はそれぞれ vec3(1, 1, 1), vec3(0, 0, 0) だから:

// Turn everything above 0.5 to white and everything below to black
vec3 color = vec3(step(.5, pct));

// Invert the color
color = 1. - color;

関数 smoothstep を使うのはよくやるのでもう大丈夫だろう。

float radius = .5;
float line_width = .01;
vec3 color = vec3(smoothstep(radius - line_width, radius, pct) - smoothstep(radius, radius + line_width, pct));

関数として定義するのはまだ早い。引数リストが確定してからだ。

色を付けるのは単に好きな色を成分ごとの乗算をすればいい。

vec3 line_color = vec3(1., 0., 0.);
// ...
color *= line_color;

円を伸び縮みさせるには、上のコード片で言うところの radiusu_time の関数にすればよい。 振幅が小さめの周期関数を採用すると良い。解答略。

円を動かすとは、文脈上円の中心を運動させるという意味しか残っていない。 円の中心を運動させるには、distance の行の実引数を工夫すればいい。

pct = distance(st,vec2(0.5 - .25 * sin(u_time), 0.5 + .1 * sin(u_time)));

以下略。

For your tool box

sqrt を可能な限り避けて dot で済ませる。 これは GLSL に限らず幾何学プログラミング全般での基本だ。

Useful properties of a Distance Field

距離場を用いて何らかの視覚的表現を実装することはよくある。

Polar shapes

正規化された座標 st からキャンバスの中心を原点とする極座標空間の座標 (r, theta) を得る公式:

vec2 pos = vec2(0.5) - st;
float r = length(pos) * 2.0;
float theta = atan(pos.y, pos.x);

演習問題

  • これらの形状をアニメーションにしろ。
  • さまざまな shaping 関数を組み合わせて形状に穴を開け、花や雪の結晶、歯車などを作れ。
  • 前章で使用した関数 plot を使って輪郭だけを描け。

解答

アニメーションは色々考えられるが、偏角 a またはプロット fu_time で加工するのが普通だろう。

const float TAU = radians(360.);

void main(){
    // ...

    float a = atan(pos.y, pos.x) - mod(u_time, TAU);
}

穴をあける問題は少し手を抜いて、白を白で引くと黒になることを利用する:

float a = atan(pos.y,pos.x) + u_time;

// ...

float cog = smoothstep(-.5, 1., cos(a * 10.)) * 0.2 + 0.5;
float hole = .2;

color = vec3(1. - smoothstep(cog, cog + 0.02, r));
color -= vec3(1. - smoothstep(hole, hole + 0.02, r));

プロットの問題は興味深い結果が得られる。前章で使用した実装はこういうものだった:

float plot(vec2 st, float pct){
    return smoothstep(pct - 0.02, pct, st.y) -
        smoothstep(pct, pct + 0.02, st.y);
}

題意を好意的に解釈して極座標用に書き直すことにする:

float plot(float r, float pct){
    return smoothstep(pct - 0.02, pct, r) -
        smoothstep(pct, pct + 0.02, r);
}

しかし、これをそのまま適用するとプロット曲線の太さが原点から遠ざかるほど太くなって不格好だ。 そこでこのように調整する。少しましになる:

float plot(float r, float pct){
    flot line_width = 0.02 / max(r, .001);
    return smoothstep(pct - line_width, pct, r) -
        smoothstep(pct, pct + line_width, r);
}

Combining powers

極座標を距離場に応用する。


演習問題

  • この節の例を使って、正多角形の位置と頂点数を入力とし、距離場の値を返す関数を書け。
  • minmax を使って距離場を混ぜ合わせろ。
  • 幾何学的なロゴを選び、距離場を使って複写しろ。

解答

最初の問題はリファクタリングに過ぎない:

float regular_polygon(int N, float size, vec2 st){
    // Angle and radius from the current pixel
    float theta = atan(st.y, st.x) + PI;
    float radius = TWO_PI / float(N);

    // Shaping function that modulate the distance
    float d = cos(floor(.5 + theta / radius) * radius - theta) * length(st);
    float e = .01;
    return 1.0 - smoothstep(size - e, size + e, d);
}

正多角形の最初の頂点が x 軸上に来るのか y 軸上に来るのかで atan 呼び出しの引数の順序を入れ替えるといい。 気に入らないのは、距離場の定義が良くないのか、頂点数によって多角形の半径がどんどん小さくなることだ。 これは正直なんとかしたい(ベクトルの問題ゆえ難しくはない)。

距離場の混ぜ合わせは max を取ると正多角形同士の和集合が現れ、min を取ると積集合が現れる。

幾何学的なロゴタイプで何かいいのがあれば試したい。

Matrices

Translate

ここはいい。

Rotations

ここはいい。

  • この節で mat2, mat3, mat4 を導入する。
  • この TeX を書いたのは誰だろう。なっていない。
  • mat2 への引数の渡し方が本文の説明と GLSL のコードとで合致していない。 GLSL では列ベクトルを並べる。このコードで見かけ上動作している理由は、 世界座標系を回転させることで、相対的にオブジェクトが反時計回りするから。 本ノートでは mat2(cos(t), sin(t), -sin(t), cos(t)) の順で統一する。

演習問題

  • 45 行目のコメントを外して、何が起こるか確かめろ。
  • 37 行目と 39 行目の回転前と回転後の並進をコメントして、その結果を確かめろ。
  • 回転を使って、並進の練習でシミュレーションしたアニメーションを改善しろ。

解答

45 行目のコメントを外すと背景にデバッグ色が着く。 これにより、座標変換がすべての画素に施されていることが納得できる。

st += vec(0.5) などをコメントアウトすると、回転中心がキャンバスの左下のままになるのがわかる。

最後の問題は並進の練習では回転ベクトルを直接計算していたので、 そこを行列の乗算に置き換えろという趣旨だろう。 ただ、どう書き換えても以前のコードのほうが保守しやすいだろう。

Scale

特にない。


演習問題

  • 42 行目のコメントを外すと、空間座標が拡縮されているのがわかる。
  • 37 行目と 39 行目の拡縮の前と後の並進をコメントするとどうなるか。
  • 回転行列と拡縮行列を組み合わせろ。順番が重要であることに注意しろ。
  • ニセの UI または HUD を設計して構築しろ。Ndel 氏による ShaderToy のが参考になる。

解答

今回もオブジェクト座標系ではなく世界座標系に対して拡縮している。 したがって、拡縮前後の並進をやめると左下(の少しずれた位置)を中心に描画全体が拡縮する。

回転を組み合わせるのは scale の前か後ろになる。 いつものように世界座標系を回転させるので、次のコードは十字の形状を時計回りに回転する。

float t = u_time;
float c = cos(t);
float s = sin(t);
mat2 R = mat2(c, s, -s, c);
mat2 S = scale(vec2(0.5, 0.5));
st = R * S * st;
st += vec2(0.5);

HUD の問題は何を言っているのかわからない。

Other uses for matrices: YUV color

YUV とは色空間の一種で、写真やビデオのアナログ符号化処理に使用される。 人間の知覚範囲を考慮して、C 成分の帯域を狭くしてある。

Patterns

  • シェーダプログラムは画素単位で実行される。形状をどれだけ反復して計算回数は一定だ。 したがって、断片シェーダーはタイルパターンに特に適していると言える。
  • 正規化した二次元座標を拡大してから fract を適用する手法が基本的だ。 これにより、正規化されたキャンバスを格子状に区切ることができる。

演習問題

  • 空間にさまざまな数値を乗算しろ。浮動小数点の値を使ったり、xy の値を変えろ。
  • このタイリング手法を使って、再利用可能な関数を作れ。
  • 空間を 3x3 に分けろ。画素がどのセルにあるかを知る方法を見つけ、それを使って表示される形を変えろ。 三目並べを構成しろ。

解答

係数として特に次の性質の数を採用する:

  • 整数でない浮動小数点数。上辺と右辺のタイルが部分しか描かれない。
  • 負の数。色の分布が反転する。

成分ごとに係数を変えると、タイルに縦横比がつき、タイルの並ぶ個数が成分別に異なるようになる。

「キャンバスを横何個かける縦何個のタイル列に分割する」関数を定義する:

vec2 tile(float scalar, in vec2 st){
    return fract(scalar * st);
}

vec2 tile(in vec2 scalar, in vec2 st){
    return fract(scalar * st);
}

これを使えばキャンバスを 3x3 に分割する処理は一行で記述できる:

tile(st, 3.);

st がどのセルに属すかは、係数の逆数を整数倍した区間のどこに属するかで決まる。 例えば scalar = 3. とする。仮に 1. / scalar <= st.x && st.x < 2. / scalar ならば st は中央の列のどこかにある。ただし st は係数倍される前の値とする。 GLSL なので floor が適任だ:

ivec2(floor(st * scalar))

さらにバツジルシを描く関数が必要だ。前章の cross を拝借すればできるだろう。 セルの条件分岐はどうする?

Apply matrices inside patterns

各セルに対して前章で習った座標変換の手法を適用することができる。


演習問題

  • このパターンをアニメーション化する面白い方法を考えろ。 色、形、動きのアニメーションを考えろ。 三種類のアニメーションを作れ。
  • さまざまな形を合成して、より複雑なパターンを作り直せ。
  • 異なるパターンのレイヤーを組み合わせて、独自のスコットランド風タータンパターンを作れ。

解答

その他の要素に関するアニメーションは学習済みなので、パターン自体をアニメーションするかどうかを中心に考えたい。

  • 分割自体を時刻によって変化させる。このコードで言うと zoom への実引数を u_time 依存にする。
  • 回転角を u_time に依存する。
  • ボックスの色を u_time に依存する。

残りの問題は難しいから後回し。

Offset patterns

関数 mod を利用した偶数奇数の判定方法を理解する。

y = step(1.0, mod(x, 2.0));

反復パターンに対するオフセット指定手法を理解する。 レンガの例では、奇数行のパターンだけ半ブロック分横にずらすという処理を示している。 関数 brickTile がそれを実現している。

st.x += step(1., mod(st.y, 2.0)) * 0.5;

演習問題

  • これを時間に応じてオフセットを移動させろ。
  • 偶数行が左に、奇数行が右に動くアニメーションを作れ。
  • この効果を列でも再現できるか。
  • X 軸と Y 軸のオフセットを組み合わせて、参考デモのように書き換えろ。

解答

関数 brickTile を順次機能拡張する方針をとる。引数 offset を追加する:

vec2 brickTile(vec2 st, float zoom, float offset){
    st *= zoom;
    st.x += step(1., mod(st.y, 2.0)) * offset;
    return fract(st);
}

呼び出し箇所を修正する:

st = brickTile(st, 5.0, fract(u_time));

偶数奇数で互い違いにオフセットさせるには、偶数、奇数を -1, 1 にそれぞれ写像することができれば十分だ。次のようにする:

vec2 brickTile(vec2 st, float zoom, float offset){
    st *= zoom;
    st.x += offset * (2. * step(.001, step(1., mod(st.y, 2.))) - 1.);
    return fract(st);
}

列で再現するには上記の xyyx に入れ替えた版を作る必要があるが、 この二つのバージョンを一つの関数にまとめられる:

vec2 brickTile(vec2 st, float zoom, float offset){
    st *= zoom;
    float limit = offset * .5;
    st += (offset * (2. * step(1., mod(st, 2.)) - 1.)
        // (1, 0) or (0, 1)
        * vec2(1. - step(.5, limit), step(.5, limit))).yx;
    return fract(st);
}

呼び出しを例えば次のように変更する:

// Apply the brick tiling
st = brickTile(st, 5.0, fract(u_time) * 2.);

Truchet Tiles

一つのセルをもう一度四つ切りに細分する。次の処理を zoom の直後に行う:

//  Scale the coordinate system by 2x2
st *= 2.;

// Give each cell an index number
// according to its position
float index = step(1., mod(st.x, 2.)) + step(1., mod(st.y, 2.)) * 2.;

//      |
//  2   |   3
//      |
//--------------
//      |
//  0   |   1
//      |

// Make each cell between 0.0 - 1.0
st = fract(st);

ここまでは分かり易い。次に細分されたセルの index に応じてパターンを変換する。

// Rotate each cell according to the index
if(index == 1.){
    // Rotate cell 1 by 90 degrees
    st = rotate2D(st, PI * 0.5);
} else if(index == 2.0){
    // Rotate cell 2 by -90 degrees
    st = rotate2D(st, PI * -0.5);
} else if(index == 3.0){
    // Rotate cell 3 by 180 degrees
    st = rotate2D(st, PI);
}

例によって if 文が気に入らないので step で書き換える。 神経質なようだが修練だと考えればいい。

st = fract(st);

float theta1 = PI * 0.5;
float theta2 = PI * -0.5;
float theta3 = PI;
float theta
    = theta1 * step(1., index)
    + (theta2 - theta1) * step(2., index)
    + (theta3 - theta2) * step(3., index);

return rotate2D(st, theta);

演習問題

  • 69 行目から 72 行目までをコメント、アンコメント、複製して新しいデザインを作れ。
  • 黒と白の三角形を、半円、回転した四角形、線などの別の要素に変えろ。
  • 要素の位置に応じて回転させる他のパターンをコードにしろ。
  • 要素の位置に応じて他の性質が変化するパターンを作れ。
  • この節の原則を適用できる、必ずしもパターンではない何か他のものを考えろ(例:易経の易卦)。

解答

コメント・アンコメント・コピー問題は色々試す。回転にアニメーションが施されているから、 二つの回転角をずらすと面白い効果を得る。

黒と白の三角形を生じているのは次に引用するコードの RGB 部分だ。これを今までのパターン定義関数に置き換える:

// step(st.x,st.y) just makes a b&w triangles
// but you can use whatever design you want.
gl_FragColor = vec4(vec3(step(st.x, st.y)), 1.0);

index に応じて「他の性質」を決めるコードを追加すればいいだろう。

最後の六十四卦をモチーフにしたパターンはコードが全部読めるのだが、コメントがないので理解に時間を要する。 有益な技法がいくつか埋め込まれているので、時間をかけて解読すれば得られるものがある。

Making your own rules

手続き型パターンの作成は、再利用可能な最小限の要素を見つけるための精神的な訓練だと著者は主張している。

  • 古代ギリシャの蛇行文様
  • 中国の格子文様
  • アラビアの幾何学模様
  • アフリカのゴージャスな布地のデザイン

パターンの世界には学ぶべきものがたくさんあるとも言っている。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment