そもそも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を用いてこれからは説明していきます。
一番最初の例
一番最初の例は単なる立方体に時間で変化するシェーダーをくっつけただけのものです。
マウスで動きます。
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
はあるGameObject
、MouseCameraComponent
はComponent
に近いことがわかることでしょう。
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"
_time
uniform変数と変化のスピードを変更する改造
_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
}
また、この値は勝手に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が出たはずです。
インスペクター使ってみる
jsdo.itではドメインの部分をjsrun.itに書き換えるだけでiframeのないページをみることができます。これを利用すればインスペクターで実際にこの画面を見ることができます。
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
などの形式などで受け取れるようになります。
さらにインスペクター側で見ればこの差は大きく見えます。
さらにこのように色をいじれるような属性が増えていることがわかることでしょう。
テクスチャを使ってみる
テクスチャ型である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
実際これは初期値なのでGOMLを書き換えることによりテクスチャを差し替えることができる。
<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
上記の例では線形合成ですが、エフェクトの描画などでありがちな加算合成にするには@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
最後に
今日紹介したのはGrimoire.jsのシェーダーの持つほんの一部の機能だけである。また、そもそもカスタムシェーダーの機能自体Grimoire.jsの一部の機能に過ぎない。
次回はさらにいろいろなGLステートをパス側から操作したり、マルチパスをやったりする内容を書こうと思う。
なお、Grimoire.jsは日本発のWebGLライブラリです。このようなシェーダーの深いところまでの知識を持つ方々が開発のコミュニティに意見などの立場からでも入っていただけると非常に助かります。
詳しくはCommunity of Grimoire.js(英語)を見てください。(実際Slackはほとんど日本語です)
補足
iPhoneなどでシェーダーがうまく動作しない報告がありますが、これはGPUの問題です。演算精度がmediumpだと桁落ちなど起こすのだと思います。
FS_PREC(mediump,float)
をFS_PREC(highp,float)
とすれば動作します
修正すべきですが、サンプルと本文とで修正する場所が多々あり少し面倒なのでこのままにしておきます。適宜自分で作る際は書き換えてください。