久々のシェーダーのため、復習をば。
シェーダー
3DCGレンダリングをするプログラムのこと。
shade は「次第に変化させる」「陰影・グラデーションをつける」などの意味があり、「shader」は頂点色やピクセル色などを次々に変化させるもの(関数)を意味する。
プログラマブルシェーダー
シェーディングをGPU上でプログラムとしてリアルタイムに実行するシェーダーのこと。
Three.js のシェーダーはGPU上で実行され、書き換え命令(プログラム)を送ることが可能なため、プログラマブルシェーダーといえる。
GLSL(OpenGL Shading Language)
Webブラウザにおいては、3DCGを表示させる際にWebGLという標準仕様を用いる。
WebGLでプログラマブルシェーダーを使うためには、C言語で開発されているGLSL という言語を用いる。
Three.jsでシェーダーを扱う方法
シェーダーをWebサイトで使用する際に最も利用されるライブラリが Three.js なので、Three.js でのシェーダーの扱いについて取り上げていく。
Three.js でシェーダーを扱う方法は2種類あり、ShaderMaterial と ShaderPass である。
ShaderMaterial は任意のシェーダーを組み込むことができるマテリアルのこと。
ShaderPass は出力した画像をポストプロセス(後処理)する仕組みのこと。
具体的には、
- WebGLRenderer で画面出力し、その結果をテクスチャに格納する
- 続く ShaderPass がそのテクスチャに対してエフェクトをかけ、次の ShaderPass に結果を渡す
2 のテクスチャのバケツリレーによって複雑なエフェクトをかけられる。
イメージとしては、PhotoShop の調整レイヤーのようなものに近い。
Three.js はこのバケツリレーを補助する EffectComposer というクラスがあり、ShaderPass と EffectComposer を併用することで、比較的簡単にポストプロセスが実現できる。
Three.js のシェーダーの種類
頂点シェーダー(vertexShader:バーテックスシェーダー)
ジオメトリ(幾何学・形状)の頂点が3Dスクリーン上のどこにあるかを計算するシェーダーのこと。
ジオメトリの動的な変形はこのシェーダーが担当する。
ベクターデータの基準点のようなイメージで、いわゆるポリゴン(面)を定義するために使用する。
3DCGではモデルはポリゴンで表示されており、ポリゴンは頂点と線で表現される。
頂点シェーダーはこの頂点情報から面をつくり、描画していく。
頂点シェーダーはレンダラーが頂点情報を参照するときに呼び出される。
ピクセルシェーダー(FragmentShader:フラグメントシェーダー)
スクリーン上のピクセルをどの色で塗るかを計算するシェーダーのこと。
テクスチャの適用やテクスチャの動的生成、ライトや陰影の適用を担当する。
頂点から面の情報をえられたら、その面を描画して画像として出力される。
ピクセルシェーダーは画像はピクセルの集合であるため、その各ピクセルを出力する際に参照される。
レンダラーは1pxずつ出力するが、その出力の度にピクセルシェーダーが実行される。
Three.js でのシェーダーの記述方法
それぞれのシェーダーは必ず main 関数を持ち、頂点シェーダーは「gl_Position」、ピクセルシェーダーは「gl_FragColor」に処理結果を格納することで最低限シェーダーとして機能する。
頂点シェーダーで使用されている position、modelMatrix、viewMatrix、projectionMatrix は Three.js の組み込み変数である。
他にもさまざまな組み込み変数が用意されている。
// ShaderMaterialを使用したシェーダーのサンプルプログラム
// 処理の順番は必ず JavaScript、VertexShader、FragmentShader の順になる
const material = new THREE.ShaderMaterial({
// 頂点シェーダーのGLSLを記述
vertexShader:
`
void main() {
vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
vec4 mvPosition = viewMatrix * worldPosition;
gl_Position = projectionMatrix * mvPosition;
}
`,
// フラグメントシェーダーのGLSLを記述
fragmentShader:
`
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`
});
GLSLの基本文法
共有可能な変数
CPUやシェーダー間で変数を共有する方法は3種類あり、それを修飾子と呼ぶ。
修飾子はGLSLのグローバル変数にのみ追加できる。
uniform 修飾子
CPU から頂点シェーダー、フラグメントシェーダーに変数を渡すことができる。
頂点シェーダーではすべての頂点で同じ変数が適用される。
uniforms は直接値を代入するのではなく value プロパティの値にする必要がある。
また uniform で受け取った値は再代入することができない。
const material = new THREE.ShaderMaterial( {
uniforms : {
amplitude: { value: 0.0 }
},
}
JavaScript から変数を渡した場合、GLSLでは下記のように型が解釈される。
- Boolean:bool
- Number:float
- THREE.Vector3:vec3
- THREE.Color:vec3
- THREE.Texture:sampler2D
https://threejs.org/docs/#api/en/core/Uniform
varying 修飾子
頂点シェーダーからフラグメントシェーダーに変数を渡すことができる。
UV座標などの受け渡しに使用する。
attribute 修飾子
CPU から頂点シェーダーへ、頂点ごとに異なる情報を渡すことができる。
uniform はすべての頂点で同じ情報を、attribute は頂点ごとに異なる情報を渡すことができる。
型の変換方法
GLSL では数値の型は厳密であり、異なる型で演算するとエラーが発生するため、その際は型の変換を行う必要がある。
float foo = 0.5 * 1; // error
float bar = 0.5 + 1.0; // OK
float cast = float(1); // 1.0
vec3 v3 = vec3(1.0); // -> [1.0, 1.0, 1.0]
vec4 v4 = vec4(v, 2.0); //-> [1.0, 1.0, 1.0, 2.0]
プリプロセッサ
Three.jsでは WebGLProgram クラスにより GPU へのシェーダー転送前に前処理が行われる。
前処理の対象となるのは、 define と include の2つで、処理結果はシェーダーに埋め込まれる(そのため、修飾子付きの変数と異なり、転送後に修正できない)。
define
上記だと、DEFINE_SAMPLE の値をシェーダー内に展開することができる。
const material = new THREE.ShaderMaterial({
defines : {
DEFINE_SAMPLE: true
}
}
また下記のように条件分岐を行うことができる。
#ifdef はdefineに応じて分岐される。
また defineと同時に展開され条件に一致しない場合は分岐ごと取り除かれるため、実行時のパフォーマンスに影響を与えない。
#ifdef DEFINE_SAMPLE
// このように #ifdef により条件分岐が可能
#endif
include
include はシェーダーにコードを展開することができる。
#include <include-sample>
「#include <チャンク名>」の形式で書かれた場所にそのままの形で展開される。
シェーダー間で共通化したい処理をチャンクに括り出し、includeすることでGLSLコードの重複を防ぐことができる。
ループ
下記のように利用する。
// forの終了条件に変数を使うことができないため注意
for(int i = 0; i < 3; i++){
//
}
Three.js 組み込みのマテリアルのカスタマイズについて
組み込みのマテリアルの定義の利用
ShaderLib というクラスに組み込みマテリアルの定義が書かれており、これを自分のコードに同じように読み込めば、組み込みのマテリアルの機能が利用できる。
組み込みシェーダーの利用
組み込みのシェーダーは src/renderers/shaders/ShaderLib に格納されているため、これを参考にしてカスタマイズする。
Uniforms の追加
また、ShaderLib には組み込みマテリアルに対応する uniform オブジェクトが定義されているため、使用できる場合はこれを再利用する。
もし自作のuniformオブジェクトを定義する場合は、UniformsUtils.mergeUniforms() 関数を利用する。
ShaderChunk の追加
src/renderers/shaders/ShaderChunk に組み込みのチャンクが定義されているため、使用できる場合はこれを再利用する。
もし自作のチャンクを定義する場合は、下記のようにチャンク名とシェーダー文字列を ShaderChunk オブジェクトに代入する。
ただし、include には名前空間がないため、名前衝突に注意する。
ShaderChunk['チャンクの名前'] = 'チャンクの内容';
// 「#include <チャンクの名前>」で利用できるようになる