はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Shadertoy,Unity】Shaderを初めて本格的に触ってみる

はじめに

私はUnityというゲームエンジンに実装されているShaderGraphというノードベースで視覚的にShaderを作れるツールを使ってShaderを作成していました。

f:id:hanaaaaaachiru:20201013140330j:plain
イメージ

ただ学校のとあるイベントでシェーダーの知識が必要になったので、コーディングの方も本格的に学んでいこうかなと考えています。

今回はShadertoyの使い方〜最低限の知識を身に着けるぐらいまで書いていきたいと思うのでよければお付き合いください。

Shadertoy

今回はブラウザ上でフラグメントシェーダーを書くことができるShadertoyを活用していきたいと思います。

いきなりUnityでコーディングしても良いですが,おまじないちっく(ちゃんと意味はあるけれど)なコードも多々含まれていて最初の取っ掛かりとしては少し辛く感じることもあります。

シェーダーの種類もたくさんありますしね。

そこでフラグメントシェーダーに特化していて個人的には取っつきやすいと思っているShadertoyを使っていこうというわけです。

ただUnityではHLSL言語ですが,ShadertoyGLSL言語であることに注意してください。

といってもそこまで大きな差はないので,さほど心配する必要はないです。
GLSLをHLSLに書き換える - Qiita

Shadertoyの開き方

Shadertoyのページを開くと,このような画面が表示されるはずです。

f:id:hanaaaaaachiru:20201013142125p:plain

右上のNewボタンを押すと,シェーダーを記述する画面へと移ります。

f:id:hanaaaaaachiru:20201013142333p:plain

初めてのシェーダー

早速初めてのシェーダーを書いてみましょう。

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // (R, G, B, A) = (1, 0, 0, 1)を各ピクセルに対して代入する
    // 例えばピクセルが 1280x720 個ある場合、1フレームで1280 x 720回この処理が呼ばれる
    fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

このコードを画面右側に箇所に書き,再生ボタンを押せば実行がされるはずです。

f:id:hanaaaaaachiru:20201013143259p:plain

正しく実行されると画面が全部赤色になります。

色の表現方法について

コードの仕組みを解説する前に、色について簡単に説明させてください。

みなさん既にご存知かと思いますが,全ての色はRGB(赤・緑・青)によって表現することができます。

f:id:hanaaaaaachiru:20201013143926p:plain
例: MediBangPaintでのカラーウィンドウ

またRGBに透明度αを追加した,RGBAという色の表現法がパソコンではよく使われています。

このRGBAはそれぞれ0 ~ 255までの値を一般的にとり,数学的に言うと4次元ベクトルであると捉えることができます。

シェーダーでの色の表現

先ほどの色の知識を利用するとコードの一部の意味が分かってきます。

数学的に4次元ベクトルと書きましたが,これがコード内のvec4に該当します。

fragColor = vec4(1.0, 0.0, 0.0, 1.0);

高級言語では色の構造体があったりなんかもよく見かけますが,GLSLはそんなリッチなものはありません。

ちなみにGLSLではvec2vec3vec4と書き,それぞれ2次元ベクトル,3次元ベクトル,4次元ベクトルです。

また以下のようにベクトルにアクセスできるのもGLSLの大きな特徴の一つになります。(少なくともシェーダー系以外で私はみたことない)

vec4 one = vec4(0.0, 1.0, 2.0, 3.0);
vec2 two = one.xy;                    // vec2(0.0, 1.0)
vec3 three = one.yzw;                 // vec3(1.0, 2.0, 3.0)
vec4 four = one.xyxy;                 // vec4(0.0, 1.0, 0.0, 1.0)
vec3 five = vec3(four.zz, three.z);   // vec3(0.0, 0.0, 3.0)


次にRGBAについてですが,0 ~ 255をとるのが一般的と言いましたがGLSLでは0.0 ~ 1.0になります。

わざわざ0.0のように少数をつけたのは「0.12144といった小数点も含むよ」という私なりの優しさです。

フラグメントシェーダーについて

今までのは軽いウォーミングアップで,ここからがすごい重要なので頑張っていきましょう。

Shadertoyはフラグメントシェーダーに特化していると最初に書きましたが,フラグメントシェーダーは「座標(x, y)を受け取り、色(R, G, B, A)を返す」ことを1フレームで全てのピクセルに対して行うことを目的としています。

この「座標(x, y)を受け取り、色(R, G, B, A)を返す」箇所を実装していくのがフラグメントシェーダーです。

f:id:hanaaaaaachiru:20201013151134p:plain


また定番のつまづきポイントの一つでもありますが,これらは1フレームで全てのピクセルに対して関数が実行されます

つまり描画領域が1280x720であれば,ピクセルは1280x720 = 921,600個もあるので関数が921,600回1フレームで呼ばれることになります。

こうやってみると物凄い膨大な量ですが、GPU君は並列処理が得意なので基本的に一瞬で処理を終了させてくれます。

mainImage関数について

今までの知識を使えばmainImage関数の概要が分かってきます。

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    fragColor = vec4(1.0, 0.0, 0.0, 1.0)
}

引数であるfragColorというのは(R, G, B, A)の4次元ベクトルで出力を意味し,fragCoord(x, y)の2次元ベクトルで入力を意味します。

このコードでは座標関係なく出力の色を赤に指定しているので,全体の画面(ピクセル全部)が赤色になったというわけですね。

座標と正規化

次に入力で入ってくるfragCoordについて詳しくみていきましょう。

これは左下を原点として,以下の画像のような関係にあります。

f:id:hanaaaaaachiru:20201013153947p:plain

ただこのまま扱おうとすると,描画領域のピクセル数の違いによって描画が異なってしまうことがあります。

これを解決するために画面の幅で割ることで正規化をすることが大切です。

vec2 uv = fragCoord/iResolution.xy;

f:id:hanaaaaaachiru:20201013154727p:plain

このiResolutionuniform変数と呼ばれるデフォルトで定義されている変数で、描画領域の幅・高さの2次元ベクトルを表しています。

ちなみにuniform変数Shader Inputsと言う箇所をクリックすると一覧をみることができます。

f:id:hanaaaaaachiru:20201013155530p:plain

実際に正規化してみる

先ほどの正規化した座標をそのまま色に出力してみましょう。

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord/iResolution.xy;
    
    fragColor = vec4(uv, 0.0, 1.0);
}

f:id:hanaaaaaachiru:20201013160444p:plain

すると人によっては定番の画像を出力することができました。

最初に少し紹介しましたが,vec4(uv, 0.0, 1.0)という書き方は以下のコードと同じ意味です。

fragColor = vec4(uv.x, uv.y , 0.0, 1.0);

時間変化をつけてみる

せっかくなので動きをつけてもっと華やかにしてあげましょう。

uniform変数の一つであるiTime(シェーダーの再生時間)をつかってみましょう。

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord/iResolution.xy;
    
    fragColor = vec4(uv.x, uv.y , abs(sin(iTime)), 1.0);
}

f:id:hanaaaaaachiru:20201013161153g:plain
RGBABの箇所ですが,ここは数学の知識が少し必要です。

abssinGLSLであらかじめ定義されている組み込み関数で,それぞれ絶対値三角関数です。
(組み込み関数の一覧はこちらー>GLSL (OpenGL ES2.0)リファレンス.md · GitHub)

これを数式で書くと |sin(t)|になり、0 ~ 1を動くようになっています。

簡単な作品

いままでの知識があれば、基本的な事はOKになったと思います。

最後に応用として少しカッコいい?ものを作って終わりにしましょう。

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord/iResolution.xy;
    
    float color = fract(uv.y * 20.0 + iTime * 10.0);
    
    fragColor = vec4(color, color , color, 1.0);
}

f:id:hanaaaaaachiru:20201013163821g:plain

一応テレビの走査線をイメージして作ってみました。

中のコードをみてみるとfractが初見かもしれませんが、小数部分を返す関数です。

イメージとしてはこんな感じ。

f:id:hanaaaaaachiru:20201013164511p:plain

あとはiTimeを足し合わせてアニメーションさせています。

もう一息

せっかくテレビの走査線っぽくしたので、画像を背景に挿入してみましょう。

まずは右下にあるiChannel0という箇所にテクスチャをセットします。

f:id:hanaaaaaachiru:20201014155455p:plain

Texture2のところに猫ちゃんの画像があるはずです。

f:id:hanaaaaaachiru:20201014155648p:plain

コードはこんな感じ。

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord/iResolution.xy;
    
    float color = fract(uv.y * 20.0 + iTime * 10.0);
    
    fragColor = vec4(color, color , color, 1.0)  * texture(iChannel0, uv);
}

f:id:hanaaaaaachiru:20201014155144g:plain
texture(iChannel0, uv)の箇所で画像を読み込み,4次元ベクトル同士を*することでRGBAの各成分ごとに掛け算(RはR同士で掛け算,GはG同士で掛け算,…)を行います。

さいごに

とりあえず基本的な箇所まで紹介をさせていただきました。

私自身シェーダーは黒魔術のイメージがあり取っつきにくそうという考えがずっとありましたが、こうやって丁寧にみていくとそこまで黒魔術ではないような気がしてます。

といっても私自身まだまだひよっこなので、よければ一緒に学んでいきましょう。

ではまた。