14
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-04

はじめに

本記事は三重大学 計算研 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 SandboxShaderToyのように,ブラウザ上のコードエディタが存在します.

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

glsl-canvas

glsl-canvas-command.png glsl-canvas-show.png

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

shader-language-support.png

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

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);
}
helloworld.png

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

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);
}
helloworld-2.png

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

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

#ifdef GL_ES
precision mediump float;
#endif

ここでは,浮動小数点演算の精度を定義しています.GLSLでは,ほとんどの演算を浮動小数点計算によって行います.最終的に出力するカラー値も,0~255のunsigned char型などではありません0.0~1.0で与えられるfloat型の値になります.精度をあげたいときはmediumphighpにすればよいですがその分速度は落ちます.逆に速度を挙げたいならば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);
}
u_time.png

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についてよりわかりやすく説明してくださっています

14
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?