Open10

春休みでShaderを完全に理解する(予定)もくもくスクラップ

ふぁゔふぁゔ

VRChatでShaderを使いたいので春休みで頑張ろうのスクラップです。
ほぼ転写ですが、手を動かして覚えていきます

ふぁゔふぁゔ

シェーダーとは?

https://thebookofshaders.com/01/?lan=jp
円や長方形などの図形を組み合わせて1文字1図形ずつ順に描画していく方法(ペイントソフトのこと...?)と違い、
コンピュータへの指示の集まりで、画面上の全てのピクセルに対して同時に指示を与える方法
ピクセルの位置を入力として色を返す関数として働き、一度に画面全体を書き出す。
(版で本を印刷するような感じ)

シェーダーはなぜ速いのか

CPU:すべてのタスクを順番に受付け、一つずつ終わらせていく
工場の作業レーン:パイプに乗ってタスクは流れていく
(このパイプがスレッドと呼ばれる)
→スクリーンの描画等膨大な処理を行う必要がある(画像コンテンツをピクセルに描画するために、ピクセルごとに計算を必要とするなど)ときかなりの負荷、足りない

そこで並列処理を行う!
大きくて強力なプロセッサー(パイプ)の代わりに、たくさんの小さなプロセッサーを動かす
GPU(Graphic Processor Unit)
例)800x600本の小さなパイプなら480,000個の処理を行うことができ、大きなデータの流れを扱うことができる。

特定の数学的な関数がハードウェアで高速に処理される(直接チップで処理!)ため、三角関数や行列計算を高速に行うことができる。

GLSLとは

GLSLはopenGL Shading Languageの略、標準化されたシェーダー言語の1つ。

ふぁゔふぁゔ

ShaderのHello World!

https://thebookofshaders.com/02/?lan=jp
サンプルコードを読む

#ifdef GL_ES
precision mediump float;
#endif

uniform float u_time;

void main() {
	gl_FragColor = vec4(1.0,0.0,1.0,1.0);
}


ショッキングピンク!

このコードから読み取れること

  • シェーダー言語は結果として色を返すmain()関数を持っている。
  • 最終的なピクセルの色は、予約語として確保されたグローバル変数gl_FragColorに割り当てられる。
  • 組み込みの「変数」や「関数」、「型」がある。
  • 不動小数点精度を持つ4次元ベクトルvec4型」が使われている。
  • C言語のように、マクロはコンパイルの前に処理される。→マクロを使える
    • グローバル変数を定義(#define)したり、#ifdef#endifを使って場合分けを行うことができる。
    • すべてのマクロの命令は**ハッシュタグ(#)**で始まる。
    • コンパイル直前にすべての#defineの呼び出しが置き換えられ、#ifdef・#endifの条件文チェックが行われる。
    • サンプルのコードでは、GL_ESが定義されているときのみ、2行目のコードを挿入する。
    • GL_ES:モバイル機器やブラウザー上でコンパイルされたことを意味する
  • 浮動小数点はシェーダーに不可欠!精度がめっちゃ大事。
    • 精度が低いとレンダリングが早くなる代わりに品質が下がる。
    • こだわりたいなら、GLSLでは浮動小数点を使うそれぞれの変数ごとに精度を指定できる。
    • 2行目のprecision mediump float;は、すべての浮動小数点型の変数に中レベルの精度を指定している。
    • より低い精度(precision lowp float;)より高い精度(precision highp float;)を選ぶこともできる。
  • 変数の自動的な型変換は保証されていない。
    • 自動的な型変換は最低限の仕様に含まれていない。
    • vec4にはfloat型の値が割り当てられる。
    • 浮動小数点型の値には小数点(.)を使うことを習慣にしよう。
    • gl_FragColor = vec4(1,0,0,1); // ERROR は不適切。
ふぁゔふぁゔ

ユニフォーム変数

https://thebookofshaders.com/03/?lan=jp
それぞれのスレッドは画像の各部分への色の割り当てを受け持っている。
シェーダーではスレッド間の情報のやり取りを行うことはできないが、CPUからそれぞれのスレッドに入力を送ることはできる。
グラフィックカードはすべてのスレッドに全く同じ入力を、読み取り専用に送るように設計されており、
スレ度はその情報を書き換えることはできない。

これらの入力uniform変数と呼ばれ、GLSLでサポートされているほとんどの型が使用できる。
サポートされている型:float、vec2、vrc3、mat2、mat3、mat4、samplar2D、samplarCubeなど...

uniform変数はシェーダーの冒頭浮動小数点精度の設定の後型指定付きで指定する。

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution; // Canvas size (width,height)
uniform vec2 u_mouse;      // mouse position in screen pixels
uniform float u_time;     // Time in seconds since load

サンプルでは一貫して

  • u_time(シェーダーが開始してから経過した秒数)
  • u_resolution(シェーダーが描画する領域の大きさ)
  • u_mouse(描画領域の中のマウスの位置)

を渡す。
他のサイトなどでは異なる名前が使用されていることがある。

ふぁゔふぁゔ

実際にやってみた

下記のコードでは、u_time(シェーダーが実行を始めてからの秒数)をサイン関数と組み合わせて使い、赤い画面の色を変化させています。

#ifdef GL_ES
precision mediump float;
#endif

uniform float u_time;

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

GPUの驚くべき機能の1つには、角度や三角関数、指数関数などがハードウェア上で高速に処理されることが挙げられます。
サポートされる関数には
sin()、 cos()、tan()、 asin()、acos()、 atan()、pow()、 exp()、log()、 sqrt()、abs()、 sign()、floor()、 ceil()、fract()、 mod()、min()、 max()、clamp() などがある。

ふぁゔふぁゔ

gl_FragCoord

GLSLにはデフォルトの入力として画面上の「フラグメント」、
つまりピクセルの位置を表すvec4 gl_FragCoordが用意されている。
スレッドが描画領域内のどこで作業をしているかを知ることができる

このgl_FragCoordはスレッドごとに異なる値を持っているためuniform変数とは呼ばず、
代わりにvarying変数と呼ぶ。

サンプル

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

void main() {
	vec2 st = gl_FragCoord.xy/u_resolution;
	gl_FragColor = vec4(st.x,st.y,0.0,1.0);
}

st.xのとき左から右に行くにつれて0.0~1.0に変化する

ふぁゔふぁゔ

アルゴリズムで絵を描く

シェイピング関数

https://thebookofshaders.com/05/?lan=jp

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

// Plot a line on Y using a value between 0.0-1.0
float plot(vec2 st) {    
    return smoothstep(0.02, 0.0, abs(st.y - st.x));
}

void main() {
	vec2 st = gl_FragCoord.xy/u_resolution;

    float y = st.x;

    vec3 color = vec3(y);

    // Plot a line
    float pct = plot(st);
    color = (1.0-pct)*color+pct*vec3(0.0,1.0,0.0);

	gl_FragColor = vec4(color,1.0);
}

vec3 color = vec3(y);では、vec3 型のコンストラクタに値を1つだけ渡している
このとき、コンストラクタは3つの色のチャンネルに同じ値を割りてようとしているのだと解釈する。

// 以下2つは同じ意味
vec3 color = vec3(1.0);
vec3 color = vec3(1.0,1.0,1.0);

// 三次元のベクトルともう1つの値(ここではalpha、つまり透明度)で初期化されている
gl_FragColor = vec4(color,1.0)
gl_FragColor = vec4(1.0,1.0,1.0,1.0)