GLSLを基本からおさらいする
画面全体を覆う板ポリに適用されたシェーダでジェネレーティブなグラフィックを作る技術のおさらい。
基本的に頂点シェーダは何もせず、フラグメントシェーダでアレコレするスタイルを取り扱う。
もっと手軽にやりたいならp5とか使ってください。
環境はglsl-canvasを前提にする。手軽でいい。
See: https://marketplace.visualstudio.com/items?itemName=circledev.glsl-canvas
シェーダはjs等に比べると厳格というか、きちんと書く必要がある。
これは(TypeScript含む)jsが緩すぎるだけだけど。
ただ型の種類は多くないし、仕様も単純で、言語としての難解さは正直ない。
難しさの大半は純粋に数学的な難解さによるところが大きい。
型 | 意味 |
---|---|
void | 値を返さない関数、または空のパラメータリスト用 |
bool | 条件型、trueまたはfalseの値を取る |
int | 符号付き整数 |
float | 単精度浮動小数点スカラー |
vec2 | 2要素の浮動小数点ベクトル |
vec3 | 3要素の浮動小数点ベクトル |
vec4 | 4要素の浮動小数点ベクトル |
bvec2 | 2要素のブールベクトル |
bvec3 | 3要素のブールベクトル |
bvec4 | 4要素のブールベクトル |
ivec2 | 2要素の整数ベクトル |
ivec3 | 3要素の整数ベクトル |
ivec4 | 4要素の整数ベクトル |
mat2 | 2×2の浮動小数点行列 |
mat3 | 3×3の浮動小数点行列 |
mat4 | 4×4の浮動小数点行列 |
sampler2D | 2Dテクスチャへアクセスするハンドル |
samplerCube | キューブマップテクスチャへアクセスするハンドル |
手始めに扱うのはfloat
とvec
。
基本的に扱うものは(正規化された)座標なんかが多いので、int
を使うことはあんまりない。
メインの計算は単一の値ならfloat
が多い。
float
は1.0
のように書く。1
とすると型エラーで怒られる。きっちりしてんな。
vec
はベクトル。その通り正しくベクトルではあるのだが、シンプルに固定長の配列のように使える。
各値へのアクセスにはショートハンドが用意されており、rgba
(色)やxy
(座標)など「ベクトルをいろんな概念に応用して上手く使えよ!」という意思を感じる。
つまりvec3 color
の各値はcolor.r
, color.g
, color.b
として直感的に色データとして扱えるし、vec2 position
はposition.x
, position.y
として座標のために扱える。ただしcolor.y
とかもできるので、そこは混乱しないように。
これはベクトルという言葉には、速度や力のような「大きさと向きをもった量」という直感的な意味のほかに、ここから転じて「要素を一列に並べたもの」という意味があるからだそう。これは大変に専門的な数学で用いられる概念らしいが、この分野がそれだけ高等な数学を要求される技術であるということでもある。
いくらか特殊な性質を持つ変数が存在する。
attribute変数
頂点属性を持つ変数。頂点シェーダでしか使用しない。
CPUからもらう。JSから渡すと考えて差し支えない。
uniform変数
汎用変数。頂点・フラグメント両方で同じ値を参照する。
CPUからもらう。JSから渡すと考えて差し支えない。
varying変数
シェーダ間の連携変数。
シェーダ間の連携とは言っても、基本的にシェーダは頂点 → フラグメントの順番に処理順が決まっているので、頂点からフラグメントへ渡す値と考えて差し支えない。
基本構造。
各種変数を宣言する。
main
という名前のvoid関数が必ず1つ必要。
main
の内部で各種計算を行い、gl_FragColor
という名前の組み込みvec4
へrgba
を代入する。
これが基本となる。
uniform vec2 u_resolution; // 画面の解像度
uniform vec2 u_mouse; // マウスの座標
uniform float u_time; // 経過時間
void main() {
// ここでなんかいろいろやる
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
とりあえず適当にグラデーションを描く。
uniform vec2 u_resolution; // 画面の解像度
void main() {
// 画面上の座標を正規化
vec2 uv = (gl_FragCoord.xy / u_resolution.xy);
// 座標の変化に応じて色数値が連続的に変化するため、グラデーションになる
vec3 color = vec3(uv.x, uv.y, 0.5);
gl_FragColor = vec4(color, 1.0); // タイルの色を設定
}
座標の正規化は脳死でやってよい。
とりあえずこれをやらないことには始まらない。
vec2 uv = (gl_FragCoord.xy / u_resolution.xy);
画面全体が柔らかなグラデーションとなる理由は、CGの特性が関係している。
CGである以上、画像は格子状のピクセルで構成されるが、フラグメントシェーダがなにをやっているかを簡潔に表すと、それぞれのピクセルが何色になるのかを求めているだけである。
gl_FragColor = vec4(color, 1.0) // RGBAを求めるだけ
そしてここがポイントであるが、GLSLは「すべてのピクセルを同時に」処理する。決してJSのイテレータのように順次処理をしない。つまり、あるピクセルのための処理の最中に、前回の(隣の)ピクセルが何色になったかを参照して...といったことはできない。すべてが同時に決まるため、単一のアルゴリズムですべてのピクセルの色を求める必要がある。
しかし、自分自身の座標については知ることができる。つまりXY座標だ。
これはピクセルごとに処理を変える(=すべてが同じ色にならない)ための重要な鍵で、この値の変化に対して色が変化するようなアルゴリズムを作成していくことで、画面全体にさまざまなグラフィックを形作ることができるようになる。
これはgl_FragCoord
という組み込み変数によって提供されるが、そのままではディスプレイ解像度をそのまま表してしまっているので、値を正規化することで画面サイズの変化を考慮した表示に変換することができる。
正規化された値は0-1の範囲であるので
- RはX軸
- GはY軸
- Bは固定値
として色を表現することで、
- X軸
- 左は赤が0なので赤みを感じない(0に近い)
- 右にいくほど赤みが増していく(1に近づく)
- Y軸
- 下は緑が0なので緑みを感じない(0に近い)
- 上にいくほど緑みが増していく(1に近づく)
のように連続的に色が変化するグラデーションが描画される。
次はタイリングする。
uniform vec2 u_resolution; // 画面の解像度
void main() {
// 画面上の座標を正規化して拡大
vec2 uv = (gl_FragCoord.xy / u_resolution.xy) * 10.0;
// 正方形のタイル座標を計算
vec2 tileUV = fract(uv);
// タイルごとに異なる色を生成する例
vec3 color = vec3(tileUV.x, tileUV.y, 0.5);
gl_FragColor = vec4(color, 1.0); // タイルの色を設定
}
正規化した座標を10倍して、fract()
にかけている。
これはタイリングのための基本的なイディオム。
これは10 * 10に分割され、各タイルの縦横比は画面サイズに同期して伸縮する。
つまりタイルのサイズが縦長になったり横長になったりするので、あまり綺麗ではない。
void main() {
// 画面上の座標を正規化
vec2 uv = (gl_FragCoord.xy / min(u_resolution.x, u_resolution.y));
// タイルの座標を計算
vec2 tileUV = fract(uv * 10.0); // タイル数を変更することで調整可能
// タイルごとに異なる色を生成する例
vec3 color = vec3(tileUV.x, tileUV.y, 0.5);
gl_FragColor = vec4(color, 1.0); // タイルの色を設定
}
このように工夫することでタイルの縦横比を固定できる。
正規化する際にmin()
で小さい方の値を使用しているので、短辺方向に10個で分割し、長辺方向の分割数は成り行きとなる。
// 上か右が成り行き
vec2 uv = (gl_FragCoord.xy / min(u_resolution.x, u_resolution.y));
// 下か左が成り行き
vec2 uv = ((gl_FragCoord.xy - u_resolution.xy) / min(u_resolution.x, u_resolution.y));
// 上下左右中央寄せ見切れ
vec2 uv = (gl_FragCoord.xy - u_resolution.xy * 0.5) / min(u_resolution.x, u_resolution.y);
またいくらかの変更を加えることで、タイルの中心と成り行きの方向を制御できる。
さて、さらっとUVという概念が登場したが、これは座標系の一種である。
ポリゴンにテクスチャを貼り付ける際に、面ごとに貼り付けるテクスチャの位置を示す座標系としてUV座標が使用される。UV座標はテクスチャ画像を左上を(0, 0)、右下を(1, 1)としたときの座標系であり、座標はUV値として面の頂点を表している。
急にポリゴンとテクスチャが登場して一気に3Dっぽくなったが、そもそもシェーダによるジェネレーティブグラフィクスは画面をピッタリ覆う板ポリゴンの表面に貼られたテクスチャによって描画するので、この文脈ではUV = 画面の座標系ということになる。これは本来の3DCG技術として捉えれば特異な状態である。
ちなみにUVという語は何かの略ではなく、一般的なXYZ座標系とは別の概念という意味で、直前の3文字であるUVWを持ち出したものである。奥行きに相当するWを除外して二次元化することでUV座標となった。
次は同心円のアニメーション。
// HSBをRGBに変換する関数。イディオムとして扱えれば充分
vec3 hsb2rgb(in vec3 c) {
vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
rgb = rgb * rgb * (3.0 - 2.0 * rgb);
return c.z * mix(vec3(1.0), rgb, c.y);
}
void main(void) {
// 画面中心で座標を正規化
vec2 position = (gl_FragCoord.xy * 2.0 - u_resolution.xy) / max(u_resolution.x, u_resolution.y);
// 座標のベクトル長を数値として取得
float l = length(position);
// HSBとして色にする
vec3 HSB = vec3(l - u_time, 0.9, 1.0);
// HSBをRGBに変換する
vec3 color = hsb2rgb(HSB);
gl_FragColor = vec4(color, 1.0);
}
この場合の座標系は中心が0,0となり、-1から1の範囲となる。
length
で取得したベクトル量とは、xyの座標方向や+-の符号を無視した純粋な量(スカラー)となるため、0-1の範囲に正規化された値となる。これをHSBのHに充てるということは、彩度と明度を固定したままに色相環を一周することに等しいため、同心円上にカラフルな模様を形成する。
これを時間経過と共に変化させることで循環させる。
モザイク。
// イディオムとして扱えば充分
vec3 hsb2rgb(in vec3 c) {
vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
rgb = rgb * rgb * (3.0 - 2.0 * rgb);
return c.z * mix(vec3(1.0), rgb, c.y);
}
void main(void) {
vec2 position = (gl_FragCoord.xy * 2.0 - u_resolution.xy) / max(u_resolution.x, u_resolution.y);
// なにかをやり始める前に、まず座標系全体を低解像度化してしまう
position = floor(position * 10.0) / 10.0;
// 座標のベクトル長を数値として取得
float l = length(position);
// HSBとして色にする
vec3 HSB = vec3(l - u_time, 0.9, 1.0);
// HSBをRGBに変換する
vec3 color = hsb2rgb(HSB);
gl_FragColor = vec4(color, 1.0);
}
10倍した座標系をfloorに渡し、その後に1/10にする。
一見意味のない処理のように見えるが、floorは渡された値よりも小さい最大の整数を返すため、これは小数点の切り捨てと考えてよい。
0.345 * 10 = 3.45;
floor(3.45) = 3;
3 / 10 = 0.3;
10倍した値をfloorにかけ、その後1/10にすることで少数点の任意の位以下を切り捨てることができる。
つまり正規化された座標系の0.Xxxxx...は全て0.Xとして振る舞うために、同じ座標の計算として全く同じ処理結果となる。
結果として、一定の範囲ごとに同じ色になる = 範囲がそのままブロック感パターンになる = 低解像度モザイク表現、という仕組みである。