カスタムマテリアルの仕様
通常、Grimoire.jsのプログラムはWebGLによってcanvasに描画されます。このため、WebGLの仕様の一つに含まれるGLSLを用いてシェーダーをカスタマイズすることができます。
直接GLSLをいじることも可能ですが、materialタグの整合性などを考慮すると、Grimoire.js用の拡張シェーダー記法(Sort(ソール、フランス語で呪文))を用いるとより良いでしょう。
この記事では、このfundamentalが実装しているGLSLの独自拡張Sort(ソール)について解説します。
マテリアルとは
.sortファイルがなんであるかを理解するためには、まずマテリアルとはなんであるかを理解する必要があります。Grimoire.jsにおいて、マテリアルとはジオメトリと同時に扱われる概念です。ジオメトリはその オリジナルの形状 を意味する概念である一方で、マテリアルはジオメトリをどのように描画するかという、描画手法の組を扱うための概念です。
一つのジオメトリは、カメラ一つであったとしても、一回のレンダリングで複数回の描画がされる可能性があります。例えば、ポストエフェクトの関係で、シーンの全体を描画する前にシーン中の物体すべての法線を書き出さなければならないかもしれません。この場合色を書き込むための手法と、法線を書き込むための手法が必要になります。
この一つあたりの手法をTechnique(テクニック)と呼びます。レンダラーが、法線の画像データが欲しい時は、すべてのマテリアルとジオメトリの組みに対して、法線の描画に対応したテクニックをもつマテリアルに対して描画命令を出します。
一つの手法は複数個の手順によって成り立っているかもしれません。例えば、3Dグラフィクスでよくある手法の一つに、あらかじめ大きめに書いたものの上に小さくした物体を書いてエッジに見せるという手法があります。手書きで言えば、下地を描いてから、上の塗りをするように一つの手法が複数個の手順によって成り立っている可能性があるのです。この手順をPass(パス)と呼びます。
つまり、一つのマテリアル(あるジオメトリを描画するための複数個の描画手法の集合)は、一つ以上のテクニック(描画手法)からなり、一つのテクニックは、一つ以上のパス(描画手順)からなります。
つまり、Material > Technique > Passの順番で包含関係があります。
一つのマテリアルを表現するsortファイルは、Techniqueを省略して記述することは可能ですが、Passを省略して書くことはできません。
まずは、カスタムマテリアルの第一歩としてパスを記述して見ましょう。
GOMLからのsortファイルの読み込み
import-material
<import-material>を用いて、記述したカスタムマテリアルを読み込むことができます。(カスタムマテリアルの記述法については後述)
1 | <import-material typeName="test" src="something.sort"/> |
このように記述すると、testという名前を用いれば、something.sortに記述されたカスタムマテリアルを用いることができます。
一度、importされたマテリアルは二つの方法で指定が可能です。
新しいマテリアルのインスタンスを作る場合
1 | <mesh material="new(test)"/> |
マテリアルを受け取りうる要素に対して、new(マテリアル名)のように記述すると、その指定先に対して新しいインスタンスが作られます。
マテリアルは読み込むマテリアルに応じて、 動的に 指定可能な属性が変化します(詳細は後述)。
例えば、あるマテリアルtestがcolor属性をColor3コンバーターによって受け取るとすればいかのような記述をすることができるようになります。
1 | <mesh material="new(test)" color="yellow"/> |
マテリアルを受け取りうる要素に対して、new(マテリアル名)のように記述すると、その指定先に対して新しいインスタンスが作られます。
マテリアルは読み込むマテリアルに応じて、 動的に 指定可能な属性が変化します(詳細は後述)。
例えば、あるマテリアルtestがcolor属性をColor3コンバーターによって受け取るとすればいかのような記述をすることができるようになります。
1 | <mesh material="new(test)" color="yellow"/> |
マテリアルタグによって共通のインスタンスを作成する場合
1 | <!--GOML直下--> |
このような記述をした場合、上記の3つのメッシュでは同じマテリアルが用いられます。<material>に指定されているcolorを変更すると3つすべてのcolor変わります。
このような指定の際は、<mesh>自身がcolorを受け取ることはないことに注意してください。
メッシュのマテリアルの初期値
ここで、一つ例としてmeshのマテリアルの初期値はnew(unlit)となっています。また、unlitというマテリアルはデフォルトで読み込まれるシェーダーの一つです。
このマテリアルは、colorとtextureという値を受け取りうるため、普段、<mesh>はこれらの値を指定できます。
つまり、マテリアルがこれ以外である時colorやtextureという属性は存在しません。
また、materialを初期値としたまま多くのメッシュを使う場合、それぞれのメッシュに対してマテリアルのインスタンスが作成されるため非効率です。
そのような際は、共通にできる部分は<material>タグを用いて共通化することを推奨します。
パスの記述
まずは、パスを記述する方法を解説します。
パスは以下の要素によって成り立ちます。
- GLSLによって記述されたシェーダー
- パスの描画前に実行されるglのステートを示した宣言文
- その他Grimoire.jsとの相互運用性のために設けられた構文
シェーダー言語の記述
シェーダーとは
シェーダー言語はGPU上で動作する言語です。WebGLの本質は3Dをできることではなく、3D描画などを高速に行えるシェーダーという言語がGPU上で動かせることにあります。
マテリアルによる頂点の移動や各ピクセルの色の決定などを高速にするために用いられます。
この特徴から、残念ながら javascriptのみではシェーダーを記述することはできません。 代わりに GLSLという言語を用いられます。
残念ながら、このページではシェーダー言語の仕様や入門について深く触れることはできません。しかし、これらは一般的にデスクトップ環境で動作するOpenGLで用いられるものと全く同じ仕様のGLSLが動作するため、既存の学習資料が幾らか存在します。
また、ShaderToyや、doxasさんの入門記事などを参照すれば、入門することができるでしょう。
sort内のシェーダー
sortのパス内にはそのまま直接シェーダーを記述することができます。
1 | @Pass{ |
GLSLで記述されたシェーダーは必ず@Passによって囲われなければなりません。
このSortによって読み込まれたシェーダーでは、頂点シェーダーとして用いる場合、#define VSが、フラグメントシェーダーとして用いる場合は#define FSが挿入されます。
これを用いることで同一ファイルで双方のシェーダーを用いることが可能になります。
例
1 |
|
あくまで、GLSLのマクロを利用したものなので、これらのVSで区切られたセクションやFSで区切られたセクションは複数回登場することもできます。
デフォルト定数
また、いくつかの定数がデフォルトで定義されます。これらの定数はjavascriptのMath.~~でアクセスできる定数と全く同じものです。
1 | // constants |
フラグメントシェーダー内でのprecision
以下のような記述をするとフラグメントシェーダーでの精度修飾子がついていないためGLSLの仕様上問題が起きます。
1 | varying vec2 vValue; |
これは、頂点シェーダー、フラグメントシェーダーともに使われるvarying vec2 vValueが先頭にあるためにfloatの精度修飾がないため問題になるからです。
本来、フラグメントシェーダーの他のどのfloat系の変数宣言よりも前にprecision float mediumpなどの記述が必要です。
しかし、単にprecision mediump float;と先頭に記述してしまえば、VSでも読み込まれてしまうのでエラーになってしまう。
そこで、FS_PRECマクロやVS_PRECマクロがあらかじめシェーダーの先頭に追加される。
それぞれの定義は以下のようになっている。
1 |
すなわち、シェーダーファイルの先頭にFS_PREC(mediump,float)と記述しておけば、実際にはフラグメントシェーダーの時のみmediump精度が用いられるようになる。
uniform変数
uniform変数、attribute変数は共にアノテーションとセマンティクスを持ちます。
意味の解説はとりあえず置いておいて、例えば以下のような記述があります。
1 | @MODELVIEWPROJECTION |
つまり、文法としては以下の形式です。
1 | @セマンティクス{アノテーション} |
セマンティクスとアノテーションを両方省略することができます。 省略した場合はセマンティクスはUSER_VALUE(例外あり、詳しくは後述)、アノテーションは空になります。上記の例では、theTextureが両方省略されています。
セマンティクスだけ省略することができます 省略した場合はセマンティクスはUSER_VALUE(例外あり、詳しくは後述)、アノテーションは空になります。 上記の例では、theColorがセマンティクスのみ省略されています。
アノテーションだけ省略することができます 省略された場合は空になります。上記の例では、matrixMVPのアノテーションだけ省略されています。
セマンティクス
セマンティクスはその変数に何が代入されるべきかということを指します。
例えば、セマンティクスがMODELVIEWPROJECTIONと記述されているときは、その変数には描画していようとしている対象のメッシュのModel - View - Projection行列が渡されます。
セマンティクスがVIEWPORTの時は、その変数には現在のビューポートの情報が渡されます。
セマンティクスによってあらかじめ、変数の受け渡しを担当するレジスター関数が決定され、パスの描画前に実行されます。
以下は、デフォルトの状態で定義されているセマンティクスのリストです。
(このうちのほとんどは、ランタイムモデルフォーマットのglTFの仕様そのものです。実は、内部的なマテリアルの保持形式はglTFの仕様にかなり近い形で保持されています。)
glTFと仕様が同じもの
| Semantic | Type | Description |
|---|---|---|
LOCAL |
FLOAT_MAT4 |
Transforms from the node’s coordinate system to its parent’s. This is the node’s matrix property (or derived matrix from translation, rotation, and scale properties). |
MODEL |
FLOAT_MAT4 |
Transforms from model to world coordinates using the transform’s node and all of its ancestors. |
VIEW |
FLOAT_MAT4 |
Transforms from world to view coordinates using the active camera node. |
PROJECTION |
FLOAT_MAT4 |
Transforms from view to clip coordinates using the active camera node. |
MODELVIEW |
FLOAT_MAT4 |
Combined MODEL and VIEW. |
MODELVIEWPROJECTION |
FLOAT_MAT4 |
Combined MODEL, VIEW, and PROJECTION. |
MODELINVERSE |
FLOAT_MAT4 |
Inverse of MODEL. |
VIEWINVERSE |
FLOAT_MAT4 |
Inverse of VIEW. |
PROJECTIONINVERSE |
FLOAT_MAT4 |
Inverse of PROJECTION. |
MODELVIEWINVERSE |
FLOAT_MAT4 |
Inverse of MODELVIEW. |
MODELVIEWPROJECTIONINVERSE |
FLOAT_MAT4 |
Inverse of MODELVIEWPROJECTION. |
MODELINVERSETRANSPOSE |
FLOAT_MAT3 |
The inverse-transpose of MODEL without the translation. This translates normals in model coordinates to world coordinates. |
MODELVIEWINVERSETRANSPOSE |
FLOAT_MAT3 |
The inverse-transpose of MODELVIEW without the translation. This translates normals in model coordinates to eye coordinates. |
VIEWPORT |
FLOAT_VEC4 |
The viewport’s x, y, width, and height properties stored in the x, y, z, and w components, respectively. For example, this is used to scale window coordinates to [0, 1]: vec2 v = gl_FragCoord.xy / viewport.zw; |
それ以外のもの
| Semantic | Type | Description |
|---|---|---|
TIME |
FLOAT |
時間(ms単位) |
HAS_TEXTURE |
BOOL |
有効なテクスチャが指定したsamplerに割り当てられているかどうか、詳細は後述 |
USER_VALUE |
ANY |
詳細は後述 |
アノテーション
セマンティクスによって、レジスター関数は決定されますが、その他に引数が必要な場合があります。
例えば、HAS_TEXTUREセマンティクスは、アノテーションの中にsamplerという引数が必要です。このHAS_TEXTUREアノテーションは、 samplerに指定されている名前の変数に、有効なテクスチャが代入されているかどうかを判定した値が代入されます。
1 | @HAS_TEXTURE{sampler:"theTexture"} |
上記の例では、theTextureに有効なテクスチャが代入されている時のみ渡されることになります。
このように、アノテーションはレジスター関数が実際の割り当て時に用いる引数のセットです。アノテーションはJSONの形式をとりますが、 キー名の"は省略可能です
USER_VALUEセマンティクス
このセマンティクスは、このuniform変数がGOMLに露出される変数であることを指します。
例えば、この記事の最初の方で記述したcolorの例がこれにあたります。<mesh>のmaterial属性など、マテリアルにGOMLから値を渡されることを示します。
例えば、USER_VALUEセマンティクスが指定されているuniform変数testがfloat型なら、つまり、
1 | @USER_VALUE |
の時、この値は<mesh>あるいは<material>に露出することになります。どちらが露出するかはどのような形でmaterialを指定したかにより異なります。new(~~)の形式で指定したなら そのメッシュ自身 、クエリ形式で指定したなら materialタグ になります。
コンバーターとdefaultアノテーション
ほかのどのGOML内のattributeとも同じように、ユーザーが渡した値をgrimoireが内部的に変換するため、コンバーターを介して実際の値は取得されます。
どのコンバーターが利用されるかは、 変数型 と アノテーション と 配列か否か によって確定します。
また、USER_VALUEセマンティクスの指定されている変数は、defaultアノテーションを受け付けることができます。
GOML側から値が指定されない場合、この値が コンバーターを通ってから 渡されることになります。
さらに、GOML側からも指定されず、defaultアノテーションによっても指定されない場合、USER_VALUEセマンティクスが指定されている場合は、それぞれの型によってきまるデフォルト値が渡されます。
つまり、
1 | GOMLによる指定値 > defaultアノテーション > 型によって決まるデフォルト値 |
によって値は解決されます。
| GLSL変数型 | コンバーター | デフォルト値 | 備考 | |
|---|---|---|---|---|
| float | Number | 0 | ||
| vec2 | Vector2 | (0,0) | ||
| vec3 | Vector3 | (0,0,0) | typeアノテーションがcolorでない時 |
|
| vec3 | Color3 | white | typeアノテーションがcolorの時 |
|
| vec4 | Vector4 | (0,0,0,0) | typeアノテーションがcolorでないとき |
|
| vec4 | Color4 | white(a=1) | typeアノテーションがcolorの時 |
|
| bool | Boolean | false | ||
| int | Number | 0 | ||
| ivec2 | Vector2 | (0,0) | ||
| ivec3 | Vector3 | (0,0,0) | ||
| ivec4 | Vector4 | (0,0,0,0) | ||
| sampler2D | Texture | 白色 1*1 のテクスチャ | ||
| mat4[] | Object | [0…0] | 型はFloat32ArrayもしくはNumberの配列を利用可能 |
この一覧にない型は現在未対応です。ただし、必要なものも多いため対応幅は順次拡大します。
デフォルトセマンティクス
利便性のため、またv0.10未満のライブラリからのアップデートの容易性を保つため、以下の変数名はデフォルトで次のセマンティクスが用いられます。
| 変数名 | セマンティクス |
|---|---|
| _time | TIME |
| _viewportSize | VIEWPORT_SIZE |
| _matL | LOCAL |
| _matM | MODEL |
| _matV | VIEW |
| _matP | PROJECTION |
| _matVM | MODELVIEW |
| _matPVM | MODELVIEWPROJECTION |
| _matIM | MODELINVERSE |
| _matIV | VIEWINVERSE |
| _matIP | PROJECTIONINVERSE |
| _matIVM | MODELVIEWINVERSE |
| _matIPVM | MODELVIEWPROJECTIONINVERSE |
| _matITM | MODELINVERSETRANSPOSE |
| _matITVM | MODELVIEWINVERSETRANSPOSE |
attribute変数
attribute変数のセマンティクス
uniform変数と同様に、attribute変数もセマンティックスを持ちます。
このセマンティクスは、どのattribute変数にジオメトリ中のどのバッファを利用すればいいのか決定するために存在します。
例えば、全てのプリミティブのジオメトリはPOSITION,NORMAL,TEXCOORDというバッファを保持しています。(もしも、自分でジオメトリを作っている方がいたとしたら、この限りではありません。)
1 | @POSITION |
と記述すれば、このvalueに、ジオメトリのPOSITIONバッファがバインドされることになります。
デフォルトセマンティクス
利便性のため、またv0.10未満のライブラリからのアップデートの容易性を保つため、以下の変数名はデフォルトで次のセマンティクスが用いられます。
| 変数名 | セマンティクス |
|---|---|
| position | POSITION |
| normal | NORMAL |
| texCoord | TEXCOORD |
つまり、以下の二つのコードは同一の意味になります。
1 | @POSITION |
1 | attribute vec3 position; |
@import文
Sort内のシェーダーでは、外部ファイルの参照ができます。@importはC++で言えば#includeのような存在です。しかし、特に特別なことはせず、単に参照先のスクリプトファイルを指定位置に挿入します。
文法
1 | @import("ファイルパス") |
ファイルパスとして受付可能なものは絶対パス及び相対パスです。一般的なURLとして動作します。
また、外部リクエストは増やしたくないが、共通のスクリプトが存在する場合は、特定のエイリアスをこのファイルパスに用いて実際にはプログラム中に既に含まれたコードないから@importを解決することができます。
このような場合、grimoirejs/lib/Material/ImportResolverをrequireして、ImageResolverのコンストラクタへの参照を取得し以下のように記述することでこれを実現可能です。
1 | ImportResolver.addAliasToStatic("ThisIsAlias","何らかのコード"); |
このように記述すると、@import("ThisIsAlias")と言う表記に出くわすと、このコードが挿入されて外部に解決を試みません。
glステートの操作
マテリアルによっては、glのステートを操作する必要があります。例えば、あるマテリアルで加算合成したい場合、本来描画する前にgl.blendFunc(gl.ONE,gl.ONE)と記述すると加算合成されます。
(ブレンディングについてはこちらのツールを利用すれば理解が捗るでしょう。)
このように、特定のglステートを操作する関数をパスの実行前に呼び出す場合、以下のような構文を記述することにより可能です。
1 | @Pass{ |
利用可能なglの関数は以下の通りです。
- BlendFunc
- BlendFuncSeparate
- BlendEquation
- BlendEquationSeparate
- BlendColor
- ColorMask
- CullFace
- DepthFunc
- DepthRange
- FrontFace
- PolygonOffset
- Scissor
また、これらを指定しなくとも初期値が読み込まれます。これらの初期値は以下の通りです。
1 | { |
また、有効になっていないGLの機能の有効、無効を切り替えることができます。
1 | @Pass{ |
このように記述すると、カリングが無効になります。(gl.disable(gl.CULL_FACE)を行うのと同じ)
つまり、裏面も描画されるようになります。
一方で、以下のように記述すればステンシルテストを有効にすることができます。(gl.enable(gl.STENCIL_TEST)を行うのと同じ)
1 | @Pass{ |
これらも、glのステートと同様に初期値が存在し以下がデフォルトでenabledとして指定されます。
- CULL_FACE
- DEPTH_TEST
- BLEND
その他の構文
マクロ
通常、GLSLでは#defineや#ifdefなどのC由来のプリプロセッサが用いれます。
Grimoire.jsではこのマクロを、GOML側の変化によって動的に変更することが可能です。
例えば、以下のような宣言がパス中に存在すると、
1 | @ExposeMacro(bool,useTexture,USE_TEXTURE,false) |
シェーダーの文中に、#define USE_TEXTURE falseや#define USE_TEXTURE trueが、GOML側のuseTexture属性によって挿入されます。
ユーザーにとって見かけ上、マテリアルのUSER_VALUE変数と同一ですが、変更された時点でシェーダーをリコンパイルするので、あまり変更が多い変数には用いられません。
しかし、配列の大きさや、forループの数など、GLSL中で定数しか用いれない場所では効果を発揮します。
また、@ExposeMacroの第一引数は型で、これによりGOML側のコンバーターが決定されますが、boolとintのみが、それぞれBooleanコンバーター、Numberコンバーターによって渡されることになります。これ以外のコンバーターに対応していないことに気をつけてください。
テクニックの記述
テクニックとは、マテリアルの中に複数の描画タイプを持っておくようにするための機構です。
1 | @Technique テクニック名{ |
のような構文をとります。
Techniqueを省略してPassを記述すると、そのTechnique名はdefaultになります
例えば、以下のようなマテリアルがあったとします。
1 | @Technique T1{ |
この際、ある<renderer>タグで以下のように指定したとします。
1 | <renderer> |
この場合、T1テクニックを持つ全てのシーン要素を描画した後、T2テクニックを持つ全てのシーン要素を描画します。
通常は、defaultテクニックが用いられるため、テクニックの指定がなくても問題ないのです。
しかし、ディファードシェーディングなど、複数回の描画を同一のメッシュに対して繰り返す場合はこの記法によって大きな威力を発揮します。
描画順序
背景にシェーダーを用いたい場合など、先に描画しておきたかったり、デプス値への書き込みをしないパーティクルなど、Grimoire.jsによる描画順序を操作したい場合があります。
1 | @Technique default{ |
のように記述すれば、このテクニックが描画される順序はNoAlphaであると言えます。
パスそれぞれに指定することはできないことに気をつけてください
また、デフォルトで指定可能な描画順序は以下の通りです。
| 描画順序名 | 優先度 | 遠くから描画 |
|---|---|---|
| Background | 1000 | しない |
| NoAlpha | 2000 | しない |
| UseAlpha | 3000 | する |
| NoDepth | 4000 | する |
| Overlay | 5000 | する |
つまり、あるテクニックが描画される際、同じテクニックを持つマテリアルは、この描画順序に基づいてレンダリングされます。
また、遠くから描画する描画順序の場合は、同じ描画順序の時、遠い方を優先し、そうでないときは近い方から描画されます。
通常、アルファ値を使う場合、遠くから描画しないと透けて見えなくなってしまいますが、使わない場合、近くから描画した方が深度テストで落ちるピクセルが多いため通常パフォーマンスが向上するはずです。
拡張
この項では、以上で定義されたデフォルトの扱いについてそれぞれの拡張の方法について議論する。
新しいUniform変数のセマンティクスを追加する
新しいUniform変数のセマンティクスを追加するには、UniformResolverRegistryクラスを用います。
以下のようにインポートします。
1 | import UniformResolverRegistry from "grimoirejs-fundamental/ref/Material/UniformResolverRegistry"; |
あるいは、
1 | var UniformResolverRegistry = gr.lib.fundamental.Material.UniformResolverRegistry; |
さらに、UniformResolverRegistry.addメソッドを用います。
1 | UniformResolverRegistry.add("新しいセマンティクス名",変数レジスターを返す関数); |
例えば、
1 | UniformResolverRegistry.add("新しいセマンティクス名",(valInfo)=>{ |
のようなことを記述すれば、このセマンティクスに対しては0が代入されることになります。
例えば、オーディオの変数を取れるようにしたりなど、この拡張性は非常に便利です。
実際には以下のコードが非常に参考になるでしょう。
https://github.com/GrimoireGL/grimoirejs-fundamental/tree/master/src/Material/Uniforms