Grimoire.jsでシェーダープログラミング入門1

  • 15
    Like
  • 2
    Comment

そもそもGrimoire.jsって何?

Grimoire.js(グリモア.js)Web開発のためのWebGLフレームワークです。

Web開発上でWebGLを用いるという選択肢が必要になった時three.jsやUnityが大きな選択肢になります。
しかし、実際には他のWebの要素を作るためには様々なWAFやライブラリ、開発ツールを使うわけですがUnityであればこれらとの連携は通常あまりなく、Webサイト場での埋め込み動画のようになってしまうのがありがちです。 一方、three.jsなどであれば確かにWebの技術で開発はできますが、Web開発の一般的な流儀であるイベントドリブンに対してループ思考の全く違う開発手法を用いる難しさに加え、最近のWAFを生かすのであればリアクティブのような考え方、設計原則を崩さずにthree.js側のロジックと組み合わせるのは至難の技であることでしょう。

実際にGrimoire.jsの思想について語ると長くなってしまう上、この記事の趣旨ではないので以下の動画に譲ることにします。

対象読者

本稿ではGLSLに関する詳細な説明は簡単なものに留めます。GLSL Sandboxなどで少しシェーダーをかじったことがあったり、その他のシェーダー周りの経験があると良さそうです。

もし、シェーダー使って面白いことできる人やGLSL Sandboxなどに投稿している方で実際にWebページ側に持って来る際に疲弊している方々がいればGrimoire.jsが強く生きる場面です。

参考リンク集

早速始めてみる

立方体を表示して立方体の表面の色を変更するコードを書いてみる。本当のところはcodepenなどのいけているjavascriptオンラインエディタでデモをやりたいが、後述する様々な理由でjsdo.itを用いてこれからは説明していきます。

一番最初の例

一番最初の例は単なる立方体に時間で変化するシェーダーをくっつけただけのものです。
マウスで動きます。
3078eaa64d913fb027ecce7ae56a0a4d.png
shader-test1

上記のリンクをみると驚くかもしれないのが、このサンプルにHTMLのコードしかないことです。

<script src="https://unpkg.com/grimoirejs-preset-basic/register/grimoire-preset-basic.js"></script>
<script type="text/sort" typeName="testMat">
@Pass{
   FS_PREC(mediump,float)
   @import "basic-vert"
   #ifdef FS
      uniform float _time;
      void main(){
         gl_FragColor = abs(vec4(sin(_time/1000. + vTexCoord.x),cos(_time/1000. + vTexCoord.y),0.5,1));
      }
   #endif
}
</script>
<script type="text/goml">
    <goml>
      <scene>
        <camera>
          <camera.components>
              <MouseCameraControl/>
          </camera.components>
        </camera>
        <mesh geometry="cube" material="new(testMat)" color="yellow"/>
      </scene>
   </goml>
</script>

この例では主に3つのscriptが存在していますが、これらについて順番に解説していきます。

1.スクリプトタグ

<script src="https://unpkg.com/grimoirejs-preset-basic/register/grimoire-preset-basic.js"></script>

このコードはCDNからGrimoire.jsを読み込んでいるコードです。ちなみにこの読み込み対象はGrimoire.js本体のみでなく、複数の標準で使うであろうプラグインが一つにまとまったパッケージです。

実際には以下の3つのライブラリがこれ一つにバンドリングされています。

  • grimoirejs (コア部分)
  • grimoirejs-math (Grimoie.js用数学プラグイン)
  • grimoirejs-fundamental (Grimoire.js用WebGLレンダラプラグイン)

2.GOML
先に一番下のscriptタグの方を見ます。

<script type="text/goml">
    <goml>
      <scene>
        <camera>
          <camera.components>
              <MouseCameraControl/>
          </camera.components>
        </camera>
        <mesh geometry="cube" material="new(testMat)" color="yellow"/>
      </scene>
   </goml>
</script>

これはgomlという、Grimoire.jsのためのDOMツリーを表すファイルです。
実際このファイルを用いてどのようなシーンを表示してカメラを置くか記述しています。
例えば<mesh position="3,0,0">と記述すればメッシュが右側に動くことになりますし、その他諸々のシーン開始時の状態をGrimoire.jsはこの形式から読み取ります。Grimoire.jsのためのHTMLのようなものですね。

実際のユースケースではscriptタグ内に直接書かず、src属性をscriptタグに指定することで外部のテキストファイルを読み込ませることでHTMLとファイルを分割することをオススメしています。そうすれば、お好みのエディタなどでシンタックスハイライトをXMLの設定にしてくれれば綺麗な感じになります。

本稿では必要最低限に止まりGOMLの詳しい話は含める予定はありませんが、簡単にわかる例として他に、カメラの以下の部分を

        <camera>
          <camera.components>
              <MouseCameraControl/>
          </camera.components>
        </camera>

以下のようにしてあげるとマウスでカメラが動かなくなります。

     <camera>
          <camera.components>
          </camera.components>
     </camera>

なぜなら、最初のカメラはMouseCameraComponentというコンポーネントをつけたカメラをだせという意味だったからです。このMouseCameraComponentがマウスでカメラを操作できるというような形式のコンポーネントになります。
Unityに詳しい人なら、cameraはあるGameObjectMouseCameraComponentComponentに近いことがわかることでしょう。

GOMLは深く理解すれば3Dコンテンツをすごく効率よく、再利用性高いコードを書くことができるのですが趣旨が外れてしまうためこの程度にしておきます。公式サイトに詳しいチュートリアル(英語)があるので参考にしていただければ嬉しいです。

3.SORT

<script type="text/sort" typeName="testMat">
@Pass{
   FS_PREC(mediump,float)
   @import "basic-vert"
   #ifdef FS
      uniform float _time;
      void main(){
         gl_FragColor = abs(vec4(sin(_time/1000. + vTexCoord.x),cos(_time/1000. + vTexCoord.y),0.5,1));
      }
   #endif
}
</script>

SORT(.sort)はGrimoire.jsのカスタムマテリアル形式です。つまり、これがシェーダーであり、本稿のメインコンテンツになります。
ちなみに読み方は「ソート」ではなく「ソール(呪文,フランス語)」になります。少し気取ってますねすいませんね。

SORTはGLSLを拡張して以下のようなことができるようになったものです。

  • attribute変数のジオメトリによる自動決定及び自動引き渡し
  • uniform変数の自動引き渡し及び簡易なユーザー側からの引数定義
  • マクロ変数の自動引き渡し及び簡易な引数定義
  • 他のシェーダーコードを含める@import機能
  • GLステート(カリング設定やアルファブレンディング設定など)をパスごとに設定する機能
  • 動的なGLステート設定機能など
  • ソーティングオーダーの設定
  • attribute変数の存在如何によるマクロ定義
  • マルチパス・マルチテクニックシェーダーコードの記述

まあ盛りだくさんにいろいろできます。GLSLそのままであることを捨てはしたものの、基本的なGLSL構文に以上の機能をサポートするための構文が追加されていることになります。本稿ではこれらの使い方について実際の実例を交えながら解説していきます。

インスペクターの利用

Grimoire.js開発をするにあたり、絶対に欠かせないのがChrome拡張として提供されているGrimoire.js Inspectorです。
Grimoireの管理している様々な値や構造をリアルタイムに確認、変更することで既存の他のHTMLのDOMのような感覚で操作することができます。

以下のページのリンクからダウンロードしていただきChromeに入れていただけると今後の進行に便利です。

インスペクターはiframeの中身をインスペクトすることができません。そのためjsrun.it側のアドレスから見ないとインスペクトできません。(この理由でcodepenは使えなかったのです)
具体的な使用例は後述します。

最初のシェーダーの例を深く見てみる

最初のサンプルで登場した以下のコードの解説をしつつ変化のスピードをいじりたいというユースケースがあったとして対応していくこととしましょう。

@Pass{
   FS_PREC(mediump,float)
   @import "basic-vert"
   #ifdef FS
      uniform float _time;
      void main(){
         gl_FragColor = abs(vec4(sin(_time/1000. + vTexCoord.x),cos(_time/1000. + vTexCoord.y),0.5,1));
      }
   #endif
}

@Passディレクティブ

SORTファイルは複数個のパスによって構成されます。(より正確には1個以上のパスを含む複数個のテクニックによって構成されますがここではテクニックについては触れません)
この@Pass内が一回分のドローコールの際に使われるシェーダーになります。

実際に生でWebGLのシェーダーコードを書いたことがある方はWebGLでは頂点シェーダとフラグメントシェーダーを記述する必要があることをご存知のことでしょう。Grimoireでは(カスタムマテリアル文法を用いる場合)二つのシェーダーコードを区別しません。ただし、頂点シェーダーとしてコンパイルされる際は頭に#define VSが、フラグメントシェーダーとしてコンパイルされる際は#define FSが付加されます。

FS_PREC(mediump,float)

発展的な内容ですので今はprecision mediump float;と同じであるという理解でいいでしょう。
よくシェーダー入門時におまじないとされるアレです。

より深く知りたい方は
このマクロはあらかじめ以下のように定義されています。

#ifdef FS
  #define FS_PREC(prec,type) precision prec type;
  #define VS_PREC(prec,type)
#endif
#ifdef VS
  #define VS_PREC(prec,type) precision prec type;
  #define FS_PREC(prec,type)
#endif

ただ単に、指定したprecisionがFSの時だけ適用されるものとVSの時だけ適用されるものが含まれるようになっているだけです。これは、VSの方が慣習的に上に書くので、その上にFSの場合精度の指定が必要のためここでも#ifdef FSと記述しコードの見通しを下げないためにあります。

@import ステートメント

 
これは存在する場所に対応するシェーダーコードを挿入するだけの命令です。
例えばデフォルトでよく使う種類の頂点シェーダーが含まれています。

  • basic-vert頂点シェーダー・・・通常の座標変換を行うための頂点シェーダー
  • screen-vert頂点シェーダー・・・一切の変換を考慮せず、通常、QUADをスクリーンにべたっと貼り付けるために使う頂点シェーダー

さらに深く知りたい場合、以下のそれぞれのシェーダーのコードのソースが参考になります。

また、このbasic-vert内に含まれる頂点シェーダーにより、以下のvarying変数が登録されています。

  • varying vec2 vTexCoord;
  • varying vec3 vPosition;
  • varying vec3 vNormal;

座標系を考慮しなければならない、vPosition及びvNormalについてはワールド座標系での値になります。

他にも、URLを取れば別のシェーダーファイルを埋め込めます。

例:

@import "basic-vert"
@import "../../shaders/something.glsl"

_timeuniform変数と変化のスピードを変更する改造

_timeと記述するだけで時間が取れているのはこのあと深く解説します。今の段階ではここにms単位の時間が入ってくるというような認識をしていただければ結構です。

変化のスピードを調整するためにuniform float speedを定義します。ここの値を_timeにかけてから利用することとしましょう。

@Pass{
   FS_PREC(mediump,float)
   @import "basic-vert"
   #ifdef FS
      uniform float _time;
      uniform float speed;
      void main(){
         gl_FragColor = abs(vec4(sin(_time/1000. * speed + vTexCoord.x),cos(_time/1000.*speed + vTexCoord.y),0.5,1));
      }
   #endif
}

しかしGLSLをおかきになったことがある方がいればわかる通り、speedに変数を渡すための長いjsのコードを本来は書かなければなりません。しかし、SORTでは以下のようにしてデフォルト値を渡すことができます。

@Pass{
   FS_PREC(mediump,float)
   @import "basic-vert"
   #ifdef FS
      uniform float _time;
      @{default:0.2}
      uniform float speed;
      void main(){
         gl_FragColor = abs(vec4(sin(_time/1000. * speed + vTexCoord.x),cos(_time/1000.*speed + vTexCoord.y),0.5,1));
      }
   #endif
}

3078eaa64d913fb027ecce7ae56a0a4d.png

実際に動くコード

また、この値は勝手にGrimoire.jsの管理するGOMLのタグに露出します。その証拠に以下のようにGOMLを書き換えてみることにしましょう。

<script type="text/goml">
    <goml>
      <scene>
        <camera>
          <camera.components>
              <MouseCameraControl/>
          </camera.components>
        </camera>
        <mesh geometry="cube" material="new(testMat)" position="2,0,0"/>
        <mesh geometry="cube" material="new(testMat)" speed="3" position="-2,0,0"/>
      </scene>
   </goml>
</script>

以下のように動くスピードの違う二つのcubeが出たはずです。

bf07efdc3a607a7dd315d41485a8c052.png

動くコード

インスペクター使ってみる

jsdo.itではドメインの部分をjsrun.itに書き換えるだけでiframeのないページをみることができます。これを利用すればインスペクターで実際にこの画面を見ることができます。

9cadd11dce029fc54f48fd8e1a93cb56.png

Chromeの拡張をインストールして入れば、このようなGrimoireのタブが出てきます。meshをクリックすることでspeedという値があるのがわかることでしょう。
この値を変えれば実際にスピードが変化するのがわかるかと思います。

少しこのサンプルではインスペクターの利点がわかりにくいので今度は色を少し変えて見ましょう。

以下のようなシェーダーに変更します。

@Pass{
   FS_PREC(mediump,float)
   @import "basic-vert"
   #ifdef FS
      uniform float _time;
      @{default:0.2}
      uniform float speed;
      @{type:"color",default:"yellow"}
      uniform vec3 color;
      void main(){
         gl_FragColor = abs(vec4(sin(_time/1000. * speed + vTexCoord.x),cos(_time/1000.*speed + vTexCoord.y),0.5,1));
         gl_FragColor.rgb *= color;
      }
   #endif
}

動くコード

vec3の変数の場合はtypeという値も取る必要があります。この値がない場合ベクトル量と見なされ、1,0,1のような指定方法でGOMLで指定することになりますが、ここをcolorにしておくことにより色の値や#FF00CCなどの形式などで受け取れるようになります。

さらにインスペクター側で見ればこの差は大きく見えます。

531b55351df34875f12f153067cf0017.png

さらにこのように色をいじれるような属性が増えていることがわかることでしょう。

テクスチャを使ってみる

テクスチャ型であるsampler2D型もfloatやvec3の場合と同様に初期値などを受け取ることができる。

以下のようにコードを変更する。

@Pass{
   FS_PREC(mediump,float)
   @import "basic-vert"
   #ifdef FS
      uniform float _time;
      @{default:0.2}
      uniform float speed;
      @{default:"http://jsrun.it/assets/I/0/4/u/I04uC.png"}
      uniform sampler2D texture;

      void main(){
         gl_FragColor = abs(vec4(sin(_time/1000. * speed + vTexCoord.x),cos(_time/1000.*speed + vTexCoord.y),0.5,1));
         gl_FragColor.rgb *= texture2D(texture,vTexCoord).rgb;
      }
   #endif

81a454ff84dcdb4bbf93576b05b5a046.png

動くコード

実際これは初期値なのでGOMLを書き換えることによりテクスチャを差し替えることができる。

99c7bd6e3b33ad56bfe104013fc74e6e.png

    <goml>
      <scene>
        <camera>
          <camera.components>
              <MouseCameraControl/>
          </camera.components>
        </camera>
        <mesh geometry="cube" material="new(testMat)" position="2,0,0" texture="http://jsrun.it/assets/G/B/2/1/GB21o.png"/>
        <mesh geometry="cube" material="new(testMat)" color="gray" speed="3" position="-2,0,0"/>
      </scene>
   </goml>

動くコード

GLステートを操作してみる(ブレンディング)

最後に半透明にしてみることにしましょう。半透明にするにはテクスチャの値をアルファ値にかけてあげるのと、GLステートの一つであるblendFuncを変更する必要があります。

   FS_PREC(mediump,float)
   @BlendFunc(SRC_ALPHA,ONE_MINUS_SRC_ALPHA) // これでこのパスを描くときにGLステートが変更される
   @import "basic-vert"
   #ifdef FS
      uniform float _time;
      @{default:0.2}
      uniform float speed;
      @{default:"http://jsrun.it/assets/I/4/6/m/I46mz.png"}
      uniform sampler2D texture;

      void main(){
         gl_FragColor = abs(vec4(sin(_time/1000. * speed + vTexCoord.x),cos(_time/1000.*speed + vTexCoord.y),0.5,1));
         gl_FragColor *= texture2D(texture,vTexCoord); // ここも変わっている
      }
   #endif

754919eb197b38b7dc29ae32e3315fce.png

動くコード

上記の例では線形合成ですが、エフェクトの描画などでありがちな加算合成にするには@BlendFunc(SRC_ALPHA,ONE_MINUS_SRC_ALPHA)@BlendFunc(ONE,ONE)にします。

Grimoire.jsのパスないではこのような一部のgl設定の関数を呼び出すことを登録することができます。それぞれ定数部分は同じ名前になっていますが、関数名が先頭大文字のキャメルケースになっています。

   FS_PREC(mediump,float)
   @BlendFunc(ONE,ONE) // これでこのパスを描くときにGLステートが変更される
   @import "basic-vert"
   #ifdef FS
      uniform float _time;
      @{default:0.2}
      uniform float speed;
      @{default:"http://jsrun.it/assets/I/4/6/m/I46mz.png"}
      uniform sampler2D texture;

      void main(){
         gl_FragColor = abs(vec4(sin(_time/1000. * speed + vTexCoord.x),cos(_time/1000.*speed + vTexCoord.y),0.5,1));
         gl_FragColor *= texture2D(texture,vTexCoord); // ここも変わっている
      }
   #endif

f1ae7f8269820c58639b33264d27fb8a.png

動くコード

最後に

今日紹介したのはGrimoire.jsのシェーダーの持つほんの一部の機能だけである。また、そもそもカスタムシェーダーの機能自体Grimoire.jsの一部の機能に過ぎない。
次回はさらにいろいろなGLステートをパス側から操作したり、マルチパスをやったりする内容を書こうと思う。

なお、Grimoire.jsは日本発のWebGLライブラリです。このようなシェーダーの深いところまでの知識を持つ方々が開発のコミュニティに意見などの立場からでも入っていただけると非常に助かります。

詳しくはCommunity of Grimoire.js(英語)を見てください。(実際Slackはほとんど日本語です)

補足

iPhoneなどでシェーダーがうまく動作しない報告がありますが、これはGPUの問題です。演算精度がmediumpだと桁落ちなど起こすのだと思います。
FS_PREC(mediump,float)FS_PREC(highp,float)とすれば動作します

修正すべきですが、サンプルと本文とで修正する場所が多々あり少し面倒なのでこのままにしておきます。適宜自分で作る際は書き換えてください。