はじめに
本記事は三重大学 計算研 Advent Calendar 2019の5日目になります.
初投稿です.
この1年間、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やShaderToyのように,ブラウザ上のコードエディタが存在します.
今回は,VS Code上でGLSLをリアルタイムレンダリングできるGLSL Canvasを使います.
導入方法は,VS Codeの拡張機能から「glsl-canvas」と検索して,インストールすれば大丈夫です.
実行はMacの場合は[command+shift+p]でコマンドパレットを開いて[> show]と打つと出てくる「Show glslCanvas」を選んでもらうと,表示できます.
また,拡張機能でShader Language Supportを導入すると、シンタックスハイライトの恩恵を得られます.
これで、一通りの環境は揃いました.
Hello World
言語の世界はとても上下関係が厳しく,新参者はまず挨拶をします.ということで挨拶をしましょう.
#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);
}
はい,すみません,冗談です.流石に文字を反映するのは骨が折れるし,挨拶するだけで骨が折れてしまったら先へ進めません.
Hello World(改)
挨拶は簡潔な方が好きです.ということで簡単な雛形を使って挨拶しましょう.
#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);
}
GLSLには明確なHello Worldを示すものはありませんので,自分なりのHello Worldを表してみました.
順に説明していきます.まずは最初の行
#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のバージョンだとこの記述がある方がいいらしいのですが,正直外したときの挙動は変わっていない気がします.(多方面に怒られそうですが,調べきれませんでしたごめんなさい)
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
ここでは,主に外部(CPU)から取得できる変数です.u_resolution
は画像サイズ,u_mouse
はマウス座標,u_time
は時間を表します.ここの変数名は,上述したShaderToyなどでは異ります(iResolution等).これらは毎フレームごとに更新され,例えばu_time
を使えば時間によって色が変わる、なんてことができます.
#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);
}
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
と省略可能です.
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
GLSL Sandbox
The Book of Shaders Shader関係の教科書的なもの
GLSL で暖を取るための準備をしよう! GLSL お役立ちマニュアル GLSLについてよりわかりやすく説明してくださっています