Shaderをかいてみよう(Hello World編)

# はじめに

本記事は[三重大学 計算研 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/calc-mie)の5日目になります．

初投稿です．
この１年間、Shaderを書くことにハマり，その面白さを布教したいなとこの記事を書いています．殆どが独学で拙い所も多いかと思いますが，その面白さが伝わって「Shaderかいてみよう」となっていただけたら幸いです．

# Shaderとは？

Shaderとは、描画を行うプログラムの総称を表します．その内部ではGPUによる並列処理が行われているため、高速な描画を可能にしています．Shaderにはいくつか種類があり、オブジェクトの頂点の座標変換を行う**Vertex Shader**，頂点の個数や関係を変化させてメッシュを変換させる**Geometry Shader**，画面、もしくはオブジェクト表面のピクセル色情報を操作して，色付けや描画を行う**Fragment Shader**などがあります．

本記事では**Fragment Shader**に関して紹介していこうと思います．

# Fragment Shader

先述の通り，Fragment Shaderはピクセルの色情報を扱うShaderです．オブジェクト表面のUV座標や時間等を変数として与えることにより，様々な表現を描画できます．

# Shaderをかいてみよう

前置きが長くなりましたが，早速Shaderを描いていきましょう．

## 導入

今回は，Shaderを書く上でオーソドックスな，OpenGLが提供するGLSL言語を使って実際に描画を行っていきます．

開発環境は何でもいいですがGPUが積んであるPCだと並列処理が高速なのでおすすめです．

自分の環境は

- OS:macOS Mojave 10.14.6
- CPU: 2.3 GHz Intel Core i5 
- RAM: 8 GB 2133 MHz LPDDR3
- Intel Iris Plus Graphics 640 1536 MB

です．GPUはIntel Graphicsですが，今回扱うくらいの処理であれば問題なく動きます．(冷却器がとてもうるさくなりますが…)

GLSLを書く環境は，[GLSL Sandbox](http://glslsandbox.com/)や[ShaderToy](https://www.shadertoy.com/)のように，ブラウザ上のコードエディタが存在します．

今回は，VS Code上でGLSLをリアルタイムレンダリングできる**GLSL Canvas**を使います．
導入方法は，VS Codeの拡張機能から「glsl-canvas」と検索して，インストールすれば大丈夫です．
実行はMacの場合は[command+shift+p]でコマンドパレットを開いて[> show]と打つと出てくる「Show glslCanvas」を選んでもらうと，表示できます．

![glsl-canvas](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454971/73156dcb-bf94-7faf-983e-b370a17388cd.png)

<img width="1552" alt="glsl-canvas-command.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454971/6df49fec-a9e8-0859-4993-2f13bcfd6272.png">
<img width="1552" alt="glsl-canvas-show.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454971/7875aff3-12a8-c60a-7263-90ace23e0423.png">

また，拡張機能で**Shader Language Support**を導入すると、シンタックスハイライトの恩恵を得られます．

<img width="1552" alt="shader-language-support.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454971/827c4f0e-c1ea-d323-4061-b0efca169789.png">

これで、一通りの環境は揃いました．

## Hello World

言語の世界はとても上下関係が厳しく，新参者はまず挨拶をします．ということで挨拶をしましょう．

```glsl
#ifdef GL_ES
precision mediump float;
#endif

#define PI 3.14159265

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

float box(in vec2 st, vec2 size){
    float f = step(abs(st.x), size.x) - (1. - step(abs(st.y), size.y));
    return max(0., f);
}

float charaH(in vec2 st, float scale){
    vec2 p = st / scale;
    return box(p + vec2(-0.35, 0.0), vec2(0.1, 0.6)) +
            box(p + vec2(0.35, 0.0), vec2(0.1, 0.6)) +
            box(p, vec2(0.4, 0.1));
}

float charaE(in vec2 st, float scale){
    vec2 p = st / scale;
    return box(p + vec2(0., 0.5), vec2(0.4, 0.1)) + 
            box(p, vec2(0.4, 0.1)) +
            box(p + vec2(0., -0.5), vec2(0.4, 0.1)) + 
            box(p + vec2(0.35, 0.), vec2(0.1 ,0.6));
}

float charaL(in vec2 st, float scale){
    vec2 p = st / scale;
    return box(p + vec2(0.35, 0.), vec2(0.1, 0.6)) + 
            box(p + vec2(0., 0.5), vec2(0.4 , 0.1));
}

float charaO(in vec2 st, float scale){
    vec2 p = st / scale;
    vec2 r = vec2(0.38, 0.46);
    vec2 ri = r - vec2(0.12);
    return step(length(p / r) - 1., min(r.x, r.y)) - 
            step(length(p / ri) - 1., min(ri.x, ri.y));
}

vec2 rotate(in vec2 st, float angle){
    return vec2(
        cos(angle) * st.x - sin(angle) * st.y,
        sin(angle) * st.x + cos(angle) * st.y
    );
}

vec2 shear(in vec2 st, float m){
    return vec2(
        st.x + m * st.y,
        st.y
    );
}

float charaW(in vec2 st, float scale){
    vec2 p = st / scale;
    return box(shear(p, PI / 16.) + vec2(0.29, 0.), vec2(0.08, 0.6)) +
            box(shear(p, -PI / 32.) + vec2(0.08, 0.), vec2(0.08, 0.6)) +
            box(shear(p, PI / 32.) + vec2(-0.08, 0.), vec2(0.08, 0.6)) +
            box(shear(p, - PI / 16.) + vec2(-0.29, 0.), vec2(0.08, 0.6));
}

float semiCircle(in vec2 st, float r){
    float f = step(length(vec2(max(0., st.x), st.y)), r) - step(st.x, 0.);
    return max(f, 0.);
}

float semiEllipse(in vec2 st, vec2 r){
    float f = step(length(st / r) - 1., min(r.x, r.y)) - step(st.x, 0.);
    return max(f, 0.);

}

float charaR(in vec2 st, float scale){
    vec2 p = st / scale;
    return semiCircle(p + vec2(-0.15, -0.25), 0.35) - semiCircle(p + vec2(-0.13, -0.25), 0.18) +
            box(p + vec2(0.15, -0.515), vec2(0.3, 0.085)) +
            box(p + vec2(0.15, 0.015), vec2(0.3, 0.085)) + 
            box(p + vec2(0.35, 0.), vec2(0.1, 0.6)) +
            box(shear(p, PI / 8.) + vec2(-0.1, 0.28), vec2(0.1, 0.32));
}

float charaD(in vec2 st, float scale){
    vec2 p = st / scale;
    return semiEllipse(p + vec2(0.03, 0.), vec2(0.35, 0.445)) - semiEllipse(p + vec2(0.03, 0.), vec2(0.25, 0.345)) +
            box(p + vec2(0.23, -0.515), vec2(0.2, 0.084)) + 
            box(p + vec2(0.23, 0.515), vec2(0.2, 0.084)) +
            box(p + vec2(0.35, 0.), vec2(0.09, 0.6));
}

float charaEXC(in vec2 st, float scale){
    vec2 p = st / scale;
    return box(p + vec2(0., -0.2), vec2( 0.1, 0.4)) + box(p + vec2(0., 0.5), vec2(0.1));
}

float HELLOWORLD(in vec2 st){
    float scale = 0.2;
    return charaH(st + vec2(1., 0.), scale) + 
            charaE(st + vec2(0.8, 0.), scale) + 
            charaL(st + vec2(0.6, 0.), scale) +
            charaL(st + vec2(0.4, 0.), scale) +
            charaO(st + vec2(0.21, 0.), scale) + 
            charaW(st + vec2(-0.1, 0.), scale) + 
            charaO(st + vec2(-0.297, 0.), scale) + 
            charaR(st + vec2(-0.5, 0.), scale) + 
            charaL(st + vec2(-0.7, 0.), scale) + 
            charaD(st + vec2(-0.9, 0.), scale) +
            charaEXC(st + vec2(-1.05, 0.), scale) +
            charaEXC(st + vec2(-1.11, 0.), scale);
}

void main(){
    vec2 st = (gl_FragCoord.xy * 2. - u_resolution.xy) / min(u_resolution.x, u_resolution.y);

    float f = HELLOWORLD(st);

    gl_FragColor = vec4(vec3(f),1.0);
}
```

<img width="1552" alt="helloworld.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454971/61c6e1ac-c363-0852-e972-ccd53f50b369.png">

はい，すみません，冗談です．流石に文字を反映するのは骨が折れるし，挨拶するだけで骨が折れてしまったら先へ進めません．

## Hello World(改)

挨拶は簡潔な方が好きです．ということで簡単な雛形を使って挨拶しましょう．

```glsl
#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 * 2. - u_resolution.xy) / 
                        min(u_resolution.x, u_resolution.y);

    gl_FragColor = vec4(vec3(st.x, st.y, 0.), 1.0);
}
```

<img width="1552" alt="helloworld-2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454971/1b4f2455-0631-9f86-e51d-debf0ba56ff7.png">

GLSLには明確なHello Worldを示すものはありませんので，自分なりのHello Worldを表してみました．

順に説明していきます．まずは最初の行

```glsl
#ifdef GL_ES
precision mediump float;
#endif
```

ここでは，浮動小数点演算の精度を定義しています．GLSLでは，ほとんどの演算を浮動小数点計算によって行います．最終的に出力するカラー値も，0~255の`unsigned char`型などではありません0.0~1.0で与えられる`float`型の値になります．精度をあげたいときは`mediump`を`highp`にすればよいですがその分速度は落ちます．逆に速度を挙げたいならば`lowp`にすればいいです．基本的には`mediump`を使います．
`#ifdef GL_ES ... #endif`ですが，GLSLのバージョンが異なる場合があるそうです．OpenGL ES 2.0以降のGLSLのバージョンだとこの記述がある方がいいらしいのですが，正直外したときの挙動は変わっていない気がします．(多方面に怒られそうですが，調べきれませんでしたごめんなさい)

```glsl
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
```

ここでは，主に外部(CPU)から取得できる変数です．`u_resolution`は画像サイズ，`u_mouse`はマウス座標，`u_time`は時間を表します．ここの変数名は，上述したShaderToyなどでは異ります(iResolution等)．これらは毎フレームごとに更新され，例えば`u_time`を使えば時間によって色が変わる、なんてことができます．

```glsl
#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 * 2. - u_resolution.xy) / 
                        min(u_resolution.x, u_resolution.y);

    gl_FragColor = vec4(vec3(st.x, st.y, sin(u_time)), 1.0);
}
```

<img width="1508" alt="u_time.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454971/75eaffc1-7c20-91d9-c15a-7df6013e0d36.png">

```glsl

void main(){
    vec2 st = (gl_FragCoord.xy * 2. - u_resolution.xy) / min(u_resolution.x, u_resolution.y);
    ...
}

```
やっとmain関数に入って来ました．
GLSLもといShader言語では毎フレームごとにスクリーン上の全ピクセルで同じ関数が走ります．その中で，自分がどのピクセルなのか(ピクセル座標)を表した組み込み変数が`vec4 gl_FragCoord`です．ここではピクセル座標を$-1.\leq x, y \leq 1.$に正規化しています．

GLSLは型に厳しい言語です．そのため，`float`変数や`vec?`変数に対して演算を行うときは型を合わせる必要があります．ここでは，`vec4`型である`gl_FragCoord`から第1，2成分の`xy`を取り出して`vec2`型にし，スカラーである`2.`を掛けているところです．

float定数を記述する際は，整数部もしくは小数部が0である場合はそれを省略することができます．`0.2`は`.2`と省略可能です．

```glsl

 gl_FragColor = vec4(vec3(st.x, st.y, 0.), 1.0);
```

最後です．ここがFragment Shaderで最も必要なところで，最終的なピクセルの色を決定します．与えられる値は$0. < r,g,b,\alpha < 1.$で,`vec4`型のそれぞれの成分ごとに割り当てた値を組み込み変数`vec4 gl_FragColor`に格納することで色を反映します．今回は$r$(red)にピクセル座標のx成分を$g$(green)にy成分を与えています．

# まとめ

前半のおふざけが過ぎてしまって長くなってしまいました．今回は導入と，サンプルコードを使ったGLSLの紹介を行いました．VS Codeを使ってGLSLを描いていましたが，ShaderToyやGLSL Sandbox等で描いてそのまま投稿，なんてのもいいかもしれませんし，この記事で少しでも興味を持っていただけたら，そちらのサイトへ飛んで素晴らしい作品を眺めつつ，暖を取る(Shaderは処理が重いのでよくPCが熱くなります)のも寒くなる冬にはいいかもしれませんね．
続きはまた後日に描いていこうと思います．

# 参考
[ShaderToy](https://www.shadertoy.com/)
[GLSL Sandbox](http://glslsandbox.com/)
[The Book of Shaders](https://thebookofshaders.com/?lan=jp) Shader関係の教科書的なもの
[GLSL で暖を取るための準備をしよう！ GLSL お役立ちマニュアル](https://qiita.com/doxas/items/5a7b6dedff4bc2ce1586) GLSLについてよりわかりやすく説明してくださっています