第2回: プログラミングと映像表現(GLSL)

目次


CPUとGPU

GPUとはGpaphics Processing Unitの略で,3Dグラフィックスを高速に描くためのパーツです.とはいいつつ,近年ではその構造を利用して,深層学習(ディープラーニング)や,他の汎用計算に利用されることも多くあります.GPUがなぜ速いのかということについては,The Book of Shadersの図がとてもわかりやすいので引用します.

The Book of Shaders: https://thebookofshaders.com/?lan=jp


図の通りですが,CPUは複雑な処理ができる代わりにタスクを1つずつ順番に終わらせていく太い土管のような存在なのに対して,GPUは単純な計算を大量に行える代わりにそれぞれ複雑な処理をするには適していない大量のストローのような存在です.TouchDesignerのインスタンシングはこのGPUの特性を使ったGPU Instancingを実装してあるおかげでリアルタイムに大量のSOPを処理することができます.


CPUのイメージ図(The Book of Shaders シェーダーとは?より引用)



GPUのイメージ図(The Book of Shaders シェーダーとは?より引用)


こちらのNVidiaのデモ動画がイメージとしては最もわかりやすいかもしれません.

GLSLについて

GLSLはOpenGL Shading Languageの略で,shaderというプログラム可能な影をつけたり頂点情報を扱ったりして見た目を調整する言語の一部です.

まずはわかりやすく,ブラウザで動くサンプルを見てみたいと思います.

また,それらを投稿できるサイトなども充実しているので,一度見てみることをお勧めします.

shaderの種類

shaderには種類があり,それぞれの性質を把握した上で使用していく必要があります.

Fragment shader

画像(image)は画素(pixel)がグリッド状に配置されて成り立っています.それぞれの画素は異なる色に調整されて,その色と場所の組み合わせによって,
一つの絵のように見えている
のです.
以下の画像は顕微鏡で観たディスプレイの画素です.よく見るとRGBが横並びになっている最小限の構成要素がグリッド状に並び,それぞれの強さが若干違います.このように私達が普段デジタルツールなどでRGBの値を決めて作ったテクスチャは最終的にこの画素の色の強さに直結していることがわかります.

画像出典元: AV Watch

OpenGLではピクセルのことをフラグメント(かけら)と呼んで,各ピクセルにつける色はフラグメントカラーと言われます.
Fragment shaderの最も重要な役割はフラグメントカラーを決定するということです.
Pixel shaderとも呼ばれ,TouchDesignerではこちらで呼ぶことにしているようです.

Vertex shader

3DCGで使用されるポリゴンは頂点と線,それらで作られる面で表現されますが,GLSLではこの頂点情報から面を作り描画します.
レンダラが頂点情報を参照し,ポイントの位置や面の貼り方などを決めレンダリングが行なわれる過程でVertex shader(頂点シェーダ)と言います.

Geometry shader

上記2種類のshaderに比べると使用頻度は激減しますが,オブジェクト内の頂点を増減させたり,プリミティブの種類を変更できたりします.

TouchDesignerで使う

TouchDesignerでshaderを使う際,は主に2つのオペレータが使用できます.

GLSL TOP

GLSL TOPではFragment shaderのみを扱います.
画像のように,GLSL TOPを出した時点で

  • glslx_pixel
  • glslx_info

という名のDATも出現します.
このglslx_pixelのほうにshaderを記述していくことになります.

また,glslx_infoにはshaderのコンパイル結果が表示されるため,エラーログなどもこちらか確認していくことになります.

GLSL MAT

GLSL MATではFragment shader,Vertex shader,Geometry shaderを記述していくことになります.

画像のように,GLSL MATを出した時点で

  • glslx_vertex
  • glslx_pixel
  • glslx_info

という名のDATも出現します.

Fragment shaderを始める

GLSL TOPのglslx_pixelには以下のように記述されています.

// Example Pixel Shader

// uniform float exampleUniform;

out vec4 fragColor;
void main()
{
    // vec4 color = texture(sTD2DInputs[0], vUV.st);
    vec4 color = vec4(1.0);
    fragColor = TDOutputSwizzle(color);
}

vecx

shaderではx次元のベクトル型変数をvecxという形で記述できます.
例えば,RGBAの4次元の色で赤を指定したいときには,

vec4 color = vec4(1.0, 0.0, 0.0, 1.0);

のように記述します.

また,各要素にアクセスする場合は,

vec4 color = vec4(1.0, 0.0, 0.0, 1.0);
float a = color.g;
vec2 b = color.rg;
vec3 c = color.gba;

float d = color.w;
vec2 e = color.yz;
vec3 f = color.yzw;

のように記述できます.

さらに,代入時は,

vec2 v = vec2(1.0, 0.0);
vec4 color = vec4(0.5, 0.5, v);

のように記述することもできます.

out vec4 fragColor

outはFragment shaderから流し出すデータを示します.つまりFragment shaderではこのfragColorにデータを代入して,最終的なピクセルの結果に反映します.

void main() {}

メイン関数であり,すべてのピクセルに対して実行されます.メイン関数で行なう最重要項目は,fragColorにRGBA色情報を4次元ベクトルで代入することです.
そのため,最終行では

fragColor = .....

のような記述がされています.

TDOutputSwizzle()

TouchDesignerではWindowsやmacOSのクロスプラットフォームな状況で不具合が起こらないために,特別に用意された関数を通す必要があります.おまじない的なものだと考えてOKです.

Fragment shaderを書く

p5.jsとの比較

唐突ですが,p5.jsで以下の画像を得られるようなスケッチを書いてみてください.

実際コードを書くとこう書けます.

function setup() {
    createCanvas(300, 300);
}

function draw() {
    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            stroke(255, 255 * x / width, 255 * (1 - y / height));
            point(x, y);
        }
    }
}

説明するまでもないですが,二重のforループによってピクセルの座標を1つ1つ計算しています.


Fragment shaderでは以下のように書けます.

// Example Pixel Shader

// uniform float exampleUniform;

uniform vec2 res;
out vec4 fragColor;
void main()
{
    // vec4 color = texture(sTD2DInputs[0], vUV.st);
    vec2 pos = gl_FragCoord.xy / res.xy;
    vec4 color = vec4(1., pos, 1.);
    fragColor = TDOutputSwizzle(color);
}

uniform vec2 resのuniformはピクセルごとに変化しない一様な変数ということを示し,ユニフォーム変数と読まれます.今回TouchDesignerのGLSL TOPではParameter windowのVectorsタブからTOP自身の解像度me.par.resolutionwme.par.resolutionhを変数としてもってきてます.

また,この解像度はビューポート解像度と言われます.


gl_FragCoordは,ピクセルの座標を示し,フラグメント座標と言われます.
フラグメント座標はビューポートの左下を原点として,右でx増加,上でy増加となります.


vec2 pos = gl_FragCoord.xy / res.xy;は何をやっているのかというと,座標を解像度で割っているということで正規化(0-1の分布にする)しているということになります.



vec4 color = vec4(1., pos, 1.)では,そのベクトルをGとBの値に代入しているため先ほどの発色のようになります.

ピクセルに寄る

先ほどの比較で明らかになった通り,これまで行なってきたプログラミングの考え方とGPUプログラミングの考え方は根本的に異なることがわかります.
人間的な感覚から脱却し,いかにピクセル的に,少し俯瞰して一気に処理するというスタンスに寄れるかどうかがshader習得のカギとなります.少しずつ慣れていきましょう.

関数の利用

shaderも様々なビルドインの関数が使用できます.
以下を記述してみます.

// Example Pixel Shader

// uniform float exampleUniform;

uniform vec2 res;
out vec4 fragColor;
void main()
{
    vec2 pos = (gl_FragCoord.xy * 2. - res.xy) / min(res.x, res.y);
    float l = .05 / length(pos);
    vec4 color = vec4(vec3(l.), 1.);
    fragColor = TDOutputSwizzle(color);
}

(gl_FragCoord.xy * 2. - res.xy) / min(res.x, res.y)は,先ほどとは少し異なります.
gl_FragCoord.xy * 2. - res.xyは,中心がゼロになりそうです.そしてmin関数は引数の最小値を示すので,全体では短い軸の解像度min(res.x, res.y)で-1.0 — 1.0 の値で収まります.


float l = .05 / length(pos);は,length関数がベクトルの長さを示すのでlength(pos)は中心から遠ざかるほど値が大きくなります.それで0.05を割り,RGBに割り当てるので見た目としては画像のようになります.


次に,こちらを書いてみます.
こちらは三角関数をうまく使ったFragment shaderの作例です.

// Example Pixel Shader

// uniform float exampleUniform;

float PI = 3.1415926535;
uniform float time;
uniform vec2 res;
out vec4 fragColor;
void main()
{
    vec2 pos = (gl_FragCoord.xy * 2. - res.xy) / min(res.x, res.y);
    vec3 destColor = vec3(0.);
    for (float i = 0.; i < 2.; i++) {
        vec2 q = pos + vec2(cos(time + i * PI), sin(time + i * PI));
        destColor += 0.05 / length(q);
    }
    vec4 color = vec4(destColor, 1.);
    fragColor = TDOutputSwizzle(color);
}

応用していくと,このようにもできます.

// Example Pixel Shader

// uniform float exampleUniform;

float PI = 3.1415926535;
uniform float time;
uniform vec2 res;
out vec4 fragColor;
void main()
{
    vec2 pos = (gl_FragCoord.xy * 2. - res.xy) / min(res.x, res.y);
    vec3 destColor = vec3(0.);
    for (float i = 0.; i < 6.; i++) {
        vec2 q = pos + vec2(cos(time + i * PI / 3.0), sin(time + i * PI / 3.0)) * 0.7 * cos(time);
        destColor += 0.05 / length(q);
    }
    vec4 color = vec4(destColor, 1.);
    fragColor = TDOutputSwizzle(color);
}