はじめに
私はUnity
というゲームエンジンに実装されているShaderGraph
というノードベースで視覚的にShader
を作れるツールを使ってShader
を作成していました。
ただ学校のとあるイベントでシェーダーの知識が必要になったので、コーディングの方も本格的に学んでいこうかなと考えています。
今回はShadertoy
の使い方〜最低限の知識を身に着けるぐらいまで書いていきたいと思うのでよければお付き合いください。
Shadertoy
今回はブラウザ上でフラグメントシェーダーを書くことができるShadertoyを活用していきたいと思います。
いきなりUnity
でコーディングしても良いですが,おまじないちっく(ちゃんと意味はあるけれど)なコードも多々含まれていて最初の取っ掛かりとしては少し辛く感じることもあります。
シェーダーの種類もたくさんありますしね。
そこでフラグメントシェーダーに特化していて個人的には取っつきやすいと思っているShadertoy
を使っていこうというわけです。
ただUnity
ではHLSL
言語ですが,Shadertoy
はGLSL
言語であることに注意してください。
といってもそこまで大きな差はないので,さほど心配する必要はないです。
GLSLをHLSLに書き換える - Qiita
初めてのシェーダー
早速初めてのシェーダーを書いてみましょう。
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); }
このコードを画面右側に箇所に書き,再生ボタンを押せば実行がされるはずです。
正しく実行されると画面が全部赤色になります。
色の表現方法について
コードの仕組みを解説する前に、色について簡単に説明させてください。
みなさん既にご存知かと思いますが,全ての色はRGB(赤・緑・青)によって表現することができます。
またRGB
に透明度αを追加した,RGBA
という色の表現法がパソコンではよく使われています。
このRGBA
はそれぞれ0 ~ 255
までの値を一般的にとり,数学的に言うと4次元ベクトルであると捉えることができます。
シェーダーでの色の表現
先ほどの色の知識を利用するとコードの一部の意味が分かってきます。
数学的に4次元ベクトルと書きましたが,これがコード内のvec4
に該当します。
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
高級言語では色の構造体があったりなんかもよく見かけますが,GLSL
はそんなリッチなものはありません。
ちなみにGLSL
ではvec2
,vec3
,vec4
と書き,それぞれ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)を返す」箇所を実装していくのがフラグメントシェーダーです。
また定番のつまづきポイントの一つでもありますが,これらは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
について詳しくみていきましょう。
これは左下を原点として,以下の画像のような関係にあります。
ただこのまま扱おうとすると,描画領域のピクセル数の違いによって描画が異なってしまうことがあります。
これを解決するために画面の幅で割ることで正規化をすることが大切です。
vec2 uv = fragCoord/iResolution.xy;
このiResolution
はuniform変数
と呼ばれるデフォルトで定義されている変数で、描画領域の幅・高さの2次元ベクトルを表しています。
ちなみにuniform変数
はShader Inputs
と言う箇所をクリックすると一覧をみることができます。
実際に正規化してみる
先ほどの正規化した座標をそのまま色に出力してみましょう。
void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 uv = fragCoord/iResolution.xy; fragColor = vec4(uv, 0.0, 1.0); }
すると人によっては定番の画像を出力することができました。
最初に少し紹介しましたが,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); }
RGBA
のB
の箇所ですが,ここは数学の知識が少し必要です。
abs
,sin
はGLSL
であらかじめ定義されている組み込み関数で,それぞれ絶対値と三角関数です。
(組み込み関数の一覧はこちらー>GLSL (OpenGL ES2.0)リファレンス.md · GitHub)
これを数式で書くとになり、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); }
一応テレビの走査線をイメージして作ってみました。
中のコードをみてみるとfract
が初見かもしれませんが、小数部分を返す関数です。
イメージとしてはこんな感じ。
あとはiTime
を足し合わせてアニメーションさせています。
もう一息
せっかくテレビの走査線っぽくしたので、画像を背景に挿入してみましょう。
まずは右下にあるiChannel0
という箇所にテクスチャをセットします。
Texture
の2
のところに猫ちゃんの画像があるはずです。
コードはこんな感じ。
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); }
texture(iChannel0, uv)
の箇所で画像を読み込み,4次元ベクトル同士を*
することでRGBA
の各成分ごとに掛け算(RはR同士で掛け算,GはG同士で掛け算,…)を行います。
さいごに
とりあえず基本的な箇所まで紹介をさせていただきました。
私自身シェーダーは黒魔術のイメージがあり取っつきにくそうという考えがずっとありましたが、こうやって丁寧にみていくとそこまで黒魔術ではないような気がしてます。
といっても私自身まだまだひよっこなので、よければ一緒に学んでいきましょう。
ではまた。