初心者による初心者のためのシェーダー芸解説

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) / Twitter )です。

本記事はUnityゲーム開発者ギルドアドベントカレンダー2の19日目の記事です。

adventar.org

今回は初心者による初心者のためのシェーダー芸解説です。

目次


どうしてシェーダー芸?


Unityゲーム開発者ギルドアドベントカレンダーですが、今回はUnityではなく。シェーダー芸にについて書こうと思います。

Unityじゃなく?なんでシェーダー芸なのか?
Unityゲーム開発者ギルドで、10月末ぐらいからシェーダー芸の勉強を初めたからです!

では、なぜ勉強を初めたのか?

なんとなく!
楽しそう
だったから!

というのもありますが。
主に3つの理由があります。

メイン目的: シェーダー をツイッターでつぶやいて見栄えを良くしたい(ゲーム画面のツイートと比べてGLSLだけで済むので楽そうだと思った)
サブ目的:ゲームの表現の幅を広げたい。マテリアル周りに詳しくなりたい、レンダリングまわり詳しくなりたい
サブ目的:VJみたいなの憧れる

という理由です。
最近会社のSlackワークスペースに #shader勉強部屋 というのを設立して
シェーダー芸を勉強中

11月から
約1ヶ月で12のシェーダーを作成しました
※最近は1週間に1つくらいのペース

シェーダー芸とは


そもそもシェーダー芸とは?
簡単に言うとシェーダーだけでリアルタイムの演出動画や作品をつくること!

フラグメントシェーダー、バーテックスシェーダーなど種別は問わないですが
基本的にシェーダー芸というとGLSLのフラグメントシェーダーを表すことが多い気がします。

今回私が作ったのもGLSLのフラグメントシェーダーです。

いろんな方の作品は以下サイトで見ることが出来ます。
GLSL SandBox
glslsandbox.com

shadertoy
www.shadertoy.com

もしくはTwitterで #shader #つぶやきGLSLなどを検索すると出ます。

何か役に立つのか?
ぶっちゃけ、表現力は上がるが、ゲーム制作にあんまり意味は無い!
楽しい!

役に立たなくてもいい!
なぜならシェーダー「芸」だから
元々「芸」って役立つものじゃないしね!

シェーダー基礎解説

※私が今回紹介するコードは Twigl で使えるコードです

次がコード
(因みにレギュレーションはClassic)

コード

precision highp float;
uniform vec2 resolution;

float circle(vec2 p, float r) {
    return length(p) - r;
}
void  main(){
    vec2 st =(gl_FragCoord.xy*2.-resolution)/min(resolution.x,resolution.y);
    float pct = circle(st,0.5);
    float d = step(pct,0.2);
    vec3 color=vec3(d);
    gl_FragColor=vec4(color,1.0);
}

普通のプログラムと全然違いますね!

何をやってるのか?というと

の表示です。

座標
をもとに模様を描いている

どういう感じで処理されるのか?
フラグメントシェーダーは、同時にすべてのピクセルが処理されます。

CPUは同時に処理するのに向いてないのでGPUを使って処理されます。

小ネタ:
最近PCを自作したのですが、CPU、メモリ、電源、マザボだけ指して、
動作確認をしたらグラフィック処理がされず画面が表示されませんでした。
なんでかな~?と思っていましたが、それもそのはずでIntelのCPUならオンボード(グラフィック機能あり)で処理されますが
RyzenのCPUはオンボードではないCPUもあり、別途グラフィックボードを購入する必要があったりします。
そのためVGA(グラフィック表示システム)がエラーを引き起こし表示出来たかったのですよね。

組み込み変数・定義の説明

GLSLには組み込み変数や定義があったりします。
今回はその一部を紹介しておこうと思います。
ついでに通常に変数についても少し触れておきます。

precision highp float; //floatの精度指定です
uniform vec2 resolution; //スクリーンのサイズの取得(変数名は別で定義可能)
gl_FragCoord→ピクセルの位置を表します
gl_FragColor→最終的なピクセルカラーです
(gl_FragColorは新しいGLSLではもう非推奨らしいけど,twgl準拠)
他にもマウスの位置を取得とか、時間の取得とか、前フレームのテクスチャの情報を保存しておくとがあります

プログラマーなら分かると思うけど……
vec2→ x,y が格納できる変数定義
vec3 → x,y,z が格納できる変数定義
vec4→ x,y,z,wが格納できる変数定義

などがあります。今回はとりあえず
これらがわかっていれば大丈夫です。

とはいえ同時にピクセル処理されると言われても
ピンと来ない方が多いと思います、

(理解しやすいように)
時間に応じて
塗りつぶしをしてみるプログラムを書いてみようと思います。

precision highp float;
uniform float time; //1secで取得 floatで時間を取得する
void main(){
    float d = 0.;
    if(gl_FragCoord.x>time*60.){
        d = 1.;
    }

    vec3 color=vec3(d);
    gl_FragColor = vec4(color,1.0);
}

bit.ly

見ての通りx軸を1秒1分単位で順番に塗り潰していく処理です。
y軸の場合はこんな感じ。 bit.ly

なんとなく
分かってきましたでしょうか?
xは左が0で右端が解像度ごとに変わり、
yは下が0で上が解像度ごとに変わります。

組み込み関数


シェーダーには組み込み関数というものがあります。
ifで処理をすると処理負荷が高いので代わりになるものが用意されています。

今回使う簡単なものを紹介いたします。

step(a, b)→
a>bだったら0.0に、それ以上なら1.0になる

length(x)→
xの長さをfloatで返す

本格的に解説に入る前に

本格的に解説に入る前に↓プログラムの解説を行います。

vec2 st = ( gl_FragCoord.xy * 2. - resolution) / min( resolution.x, resolution.y );

解像度xyを取得しmin関数でで小さい方の数値を取得し割ります。
数値を表示すると画面では以下のような感じになります。

まずlengthの説明

軽くlengthの説明をしましたがもう少し深いlengthの話をしようと思います。
length(x)は
xの長さをfloatで返すとのことでしたが、一番最初のスクリプトを短くしてlengthにだけ焦点をあてて見るとこういった↓スクリプトが出来上がります。

precision highp float;
uniform vec2 resolution;
void  main(){
    vec2 st =(gl_FragCoord.xy*2.-resolution)/min(resolution.x,resolution.y);
    float pct = length(st); //関数内にあったlengthをそのまま利用
    vec3 color=vec3(pct);
    gl_FragColor=vec4(color,1.0);
}

分かりやすいですね。
stの中心位置はxとyが共に0に近いので0、それぞれ中心を離れるほど1に近づきます。

では、最初のスクリプトに近づけて、circle関数を作って-0.5をしてみましょう。

precision highp float;
uniform vec2 resolution;

float circle(vec2 p, float r) {
    return length(p) - r;
}
void  main(){
    vec2 st =(gl_FragCoord.xy*2.-resolution)/min(resolution.x,resolution.y);
    float pct = circle(st,0.5);
    vec3 color=vec3(pct);
    gl_FragColor=vec4(color,1.0);
}

するとこうなりました。
何が変わったのかというと、-0.5された文だけ円の黒い所が広がりました。
ソレだけですね。

今回のlengthはこれだけにのみ使っています。

stepの説明

これでスクリプトの半分は何をしているのか分かりました。
ではstepは何をやっているのでしょうか?

step(a, b)は
a>bだったら0.0に、それ以上なら1.0になる

でした。
つまり……。

precision highp float;
uniform vec2 resolution;

float circle(vec2 p, float r) {
    return length(p) - r;
}
void  main(){
    vec2 st =(gl_FragCoord.xy*2.-resolution)/min(resolution.x,resolution.y);
    float pct = circle(st,0.5);
    float d = step(pct,0.2);    //ココを追加、a>bだったら0.0に、それ以上なら1.0になる
    vec3 color=vec3(d);
    gl_FragColor=vec4(color,1.0);
}

これで元の画像に!

これからシェーダーを勉強する場合

ネットの色んなシェーダーをパクって改変して覚えていくことを
私は推奨します!
覚えたコードは保存しておくと便利!

例えばさっきの
シェーダーを改変する
ならこうです

precision highp float;
uniform vec2 resolution;
uniform float time;
float circle(vec2 p, float r) {
    return length(p) - r;
}
void  main(){
    vec2 st =(gl_FragCoord.xy*2.-resolution)/min(resolution.x,resolution.y);
    float pct = circle(st,0.5);
    float d = step(sin(time*4.0)*0.5+0.7,pct);  //a>bだったら0.0に、それ以上なら1.0になる
    vec3 color=vec3(d);
    gl_FragColor=vec4(color,1.0);
}

↓時間とsinを使うことで円を拡大縮小することが出来ます。 bit.ly

サンプルコードを2つ解説


参考までに私が書いたコードを紹介します。
口頭で説明したほうが早いのですが、ブログなので長文で解説させて頂きます。

precision highp float;
uniform vec2 resolution;
uniform float time;
mat2 rotate2d(float _angle){
    return mat2(cos(_angle),-sin(_angle),
        sin(_angle),cos(_angle));
}
float square(vec2 p) {
    return abs(p.x) + abs(p.y);
}
void main(){
  vec2 r = resolution,p = (2.*gl_FragCoord.xy - r) / min(r.x,r.y);
  float a = sin(time * 5.0) * 0.5 + 0.5;
  vec2 d = mix(vec2(length(p),0.7), vec2(square(p* rotate2d(radians(time*180.))),0.5), a);  //線形補間
  vec3 color = vec3(step(0.5,d.x));
  gl_FragColor = vec4(color,1.0);
}

bit.ly

まず、rotate2d関数は回転を表しています。2次元行列を使ってxとyの値を入れ替えています。
_angleと書いてあるとおり角度によってどのぐらい入れ替えるかが決定します。
今回はradians(time*180.)の値を入れているので1秒間に180度回転するってことですね。
mix(a,b,c)というのがありますね。
こちらはaとbどちらをどのぐらい表示するかをcの値で線形補間で遷移させるものです。
左のlengthは先程もでた円ですので右側が資格になります。
つまりのこるsquare関数は回転を止めていただくと分かりやすいですが、ダイヤ型の正方形を表示させる命令式です。

どうでしょうか?いっけん難しそうに見えますが、パーツに別ければ理解できそうな気がしてきませんか?

因みに回転に使われているSinCosはこんな動きになります。
三角関数で使われるものですが、この程度の話であれば深く考える必要はないと思います。
徐々に慣れていきましょう。

もう一つ解説


もう一つ解説してみようと思います。

precision highp float;
uniform vec2 resolution;
uniform float time;
void main(){
 vec2 r=resolution,p=(gl_FragCoord.xy*2.-r)/min(r.x,r.y);
 float d = length(p);
 d = (10.* d)+time*-10.;
 d = abs(sin(d));
 d = step(0.5,d);
 gl_FragColor = vec4(vec3(d,0.0,0.0),1.0);
}

bit.ly

まず一番簡単な一番最後のvec4(vec3(d,0.0,0.0),1.0)の部分。
こちらですがRGBAとなっており、色を表しています。目がチカチカしますね。
time-10.部分がtimeの速さになります。
10.
dの部分は円の太さです。
ここはlengthから値を取得してきて×10した値の絶対値(abs)をsin関数に入れて-1~1の値で遷移させ、
最終的にstepできっぱりと別れるように色分けしています。
簡単ですね。

と言った感じで色々シェーダーを見ると複雑なシェーダーも簡単なものなら読み解くことが出来ます。

まとめ


今回は2Dシェーダーの解説しかしなかったけれど、
シェーダーは奥が深いです。
現在私は3Dの表示(レイマーチング)の勉強をしているけれど、またまだやることが多いです。

最近はUnityや3Dソフト内でノードベースのシェーダーの表示処理がありますが、
やっていることはプログラムと同じな上、ノードベースであるぶんやれる表現に限りがあります。

色々表現手段を増やすためや処理負荷を下げるためにプログラマはシェーダーの勉強が欠かせません。

また、表現手段が増えなくても自分が書いたコードが動くのは見ていて楽しいです。
皆さんもこれをきっかけにシェーダーの勉強をやってみてはいかがでしょうか?
シェーダー芸は楽しいぞ!
以上!やまだたいしでした。

参考資料


www.iquilezles.org

qiita.com

qiita.com

docs.google.com

その他のシェーダー芸


orotiyamatano.hatenablog.com