シェーダーの概要

はじめに

このドキュメントの基礎となる Three.js の概要をまずお読みください。

今回はシェーダーについて説明します。WebGL は素晴らしい技術です。以前にも述べたように、Three.js(およびその他のライブラリ)は、その難しさを抽象化するのに非常に優れています。しかし、特定の効果を実現したい場合や、画面に表示された素晴らしいものがどのようにして実現されたのかを詳しく知りたい場合は、シェーダーがその方程式の一部になることはほぼ確実です。前回のチュートリアルで基本的な内容を学んだら、少し難しい内容に進んでみましょう。

シェーダーの起動に関して多くの処理を自動で行ってくれる Three.js を使用していることを前提として説明します。まずシェーダーのコンテキストについて説明し、このドキュメントの後半で高度な手順について説明します。

1. 2 つのシェーダー

WebGL では固定パイプラインを使用できません。つまり、すぐに使えるレンダリング手段が提供されていません。代わりに、より強力ですが、理解と使用が難しいプログラマブル パイプラインが提供されます。要するに、プログラマブル パイプラインとは、プログラマーが頂点などを画面にレンダリングする責任を負うことを意味します。シェーダーはこのパイプラインの一部であり、次の 2 種類があります。

  1. 頂点シェーダー
  2. フラグメント シェーダー

どちらも単独では意味をなさないことは、おわかりいただけると思います。両方ともグラフィック カードの GPU で完全に実行されるということを知っておく必要があります。つまり、できるだけ多くの処理をオフロードして、CPU が他の処理を行えるようにします。最新の GPU はシェーダーが必要とする関数向けに高度に最適化されているため、使用できると非常に便利です。

2. 頂点シェーダー

標準的なプリミティブ シェイプ(球など)を使用します。頂点で構成されています。頂点シェーダーには、これらの頂点が 1 つずつ順番に渡され、それらを操作できます。各頂点に対して実際に何を行うかは頂点シェーダー次第ですが、頂点シェーダーには 1 つの責任があります。それは、画面上の頂点の最終位置である 4D 浮動小数点ベクトル gl_Position をどこかの時点で設定しなければならないということです。これは非常に興味深いプロセスです。3D 位置(x、y、z の頂点)を 2D 画面に取得または投影することについて説明します。幸いなことに、Three.js などのライブラリを使用している場合は、gl_Position を設定する簡単な方法があり、処理が重くなりすぎることはありません。

3. フラグメント シェーダー

頂点を持つオブジェクトがあり、それらを 2D 画面に投影しましたが、使用する色はどうでしょうか?テクスチャと照明はどうですか?フラグメント シェーダーはまさにそのためにあります。頂点シェーダーとよく似ていますが、フラグメント シェーダーにも必須のジョブが 1 つだけあります。それは、フラグメントの最終的な色である別の 4D 浮動小数点ベクトル gl_FragColor 変数を設定または破棄することです。

では、フラグメントとは何でしょうか。三角形を構成する 3 つの頂点を考えます。その三角形内の各ピクセルを描画する必要があります。フラグメントは、その三角形の各ピクセルを描画するために、それら 3 つの頂点によって提供されるデータです。このため、フラグメントは構成する頂点から補間された値を受け取ります。ある頂点が赤で、その隣の頂点が青の場合、色の値は赤から紫、青へと補間されます。

4. シェーダー変数

UniformsAttributesVaryings の 3 つの変数宣言を行うことができます。これらの言語は、他の一般的な言語と一致しないため、混乱を招く可能性があります。以下に、それぞれの考え方を示します。

  1. ユニフォームは、頂点シェーダーとフラグメント シェーダーの両方に送信され、レンダリングされるフレーム全体で同じ値が含まれます。たとえば、ライトの位置がこれに該当します。

  2. 属性は、個々の頂点に適用される値です。属性は頂点シェーダーでのみ使用できます。たとえば、各頂点に異なる色を割り当てることができます。属性は頂点と 1 対 1 の関係にあります。

  3. バリエーションは、フラグメント シェーダーと共有する頂点シェーダーで宣言された変数です。これを行うには、頂点シェーダーとフラグメント シェーダーの両方で、同じ型と名前のバリアブル変数を宣言します。この典型的な使用例は、照明の計算で使用できる頂点の法線です。

後で、3 つのタイプすべてを使用します。これにより、実際にどのように適用されるかを確認できます。

いいえ。作成できる最もシンプルなシェーダーの 1 つを見てみましょう。

5. Bonjourno World

これが頂点シェーダーの Hello World です。

/**
* Multiply each vertex by the model-view matrix
* and the projection matrix (both provided by
* Three.js) to get a final vertex position
*/
void main() {
gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(position,1.0);
}   

フラグメント シェーダーのコード例を次に示します。

/**
* Set the color to a lovely pink.
* Note that the color is a 4D Float
* Vector, R,G,B and A and each part
* runs from 0.0 to 1.0
*/
void main() {
gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);
}

複雑すぎないですよね?

頂点シェーダーでは、Three.js からいくつかのユニフォームが送信されます。この 2 つのユニフォームは、モデルビュー行列と投影行列と呼ばれる 4D 行列です。これらの仕組みを正確に理解する必要はありませんが、可能であれば、仕組みを理解しておくことをおすすめします。簡単に言うと、頂点の 3D 位置を画面上の最終的な 2D 位置に実際に投影する方法です。

Three.js がシェーダー コードの先頭に自動的に追加するため、例のコード スニペットでは省略しています。実際には、ライトデータ、頂点カラー、頂点法線など、それ以上のものが追加されます。Three.js を使用せずにこれを行う場合は、これらのユニフォームと属性をすべて自分で作成して設定する必要があります。実話です。

6. MeshShaderMaterial を使用する

シェーダーを設定しましたが、Three.js で使用するにはどうすればよいでしょうか?

/**
* Assume we have jQuery to hand and pull out
* from the DOM the two snippets of text for
* each of our shaders
*/
var shaderMaterial = new THREE.MeshShaderMaterial({
vertexShader:   $('vertexshader').text(),
fragmentShader: $('fragmentshader').text()
});

そこから Three.js が、そのマテリアルを適用したメッシュにアタッチされたシェーダーをコンパイルして実行します。これより簡単な方法はありません。おそらくそうでしょうが、ここではブラウザで実行される 3D について説明しているので、ある程度の複雑さは想定していると思います。

MeshShaderMaterial には、uniforms と attributes という 2 つのプロパティを追加できます。どちらもベクトル、整数、浮動小数点数を取ることができますが、前述のように、ユニフォームはフレーム全体またはすべての頂点で同じであるため、単一の値になる傾向があります。ただし、属性は頂点ごとの変数であるため、配列であることが想定されます。属性配列内の値の数とメッシュ内の頂点の数との間には、1 対 1 の関係が必要です。

7. 次のステップ

次に、アニメーション ループ、頂点属性、ユニフォームを追加します。また、頂点シェーダーがフラグメント シェーダーにデータを送信できるように、可変変数も追加します。最終的に、ピンク色の球体は上と横から照らされているように見え、脈動します。少しトリッキーですが、3 つの変数型と、それらが互いにどのように関連し、基盤となるジオメトリとどのように関連しているかを理解するのに役立つはずです。

8. 偽のライト

次に、単色のオブジェクトにならないように色付けを更新します。Three.js が照明をどのように処理するかを確認することもできますが、それは必要以上に複雑なので、今回はフェイクで済ませます。Three.js の一部である素晴らしいシェーダーと、Chris Milk と Google による素晴らしい WebGL プロジェクトである Romeシェーダーをぜひご覧ください。シェーダーの話に戻りましょう。各頂点の法線をフラグメント シェーダーに提供するように、頂点シェーダーを更新します。このため、次のようなさまざまな要素を考慮します。

// create a shared variable for the
// VS and FS containing the normal
varying vec3 vNormal;

void main() {

// set the vNormal value with
// the attribute value passed
// in by Three.js
vNormal = normal;

gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(position,1.0);
}

フラグメント シェーダーでは、同じ変数名を設定し、頂点法線と球の上と右から光が当たっていることを表すベクトルの内積を使用します。この結果、3D パッケージの方向性のあるライトのような効果が得られます。

// same name and type as VS
varying vec3 vNormal;

void main() {

// calc the dot product and clamp
// 0 -> 1 rather than -1 -> 1
vec3 light = vec3(0.5,0.2,1.0);
    
// ensure it's normalized
light = normalize(light);

// calculate the dot product of
// the light to the vertex normal
float dProd = max(0.0, dot(vNormal, light));

// feed into our frag color
gl_FragColor = vec4(dProd, dProd, dProd, 1.0);

}

内積が機能する理由は、2 つのベクトルが与えられたときに、2 つのベクトルがどの程度類似しているかを示す数値が出力されるためです。正規化されたベクトルでは、ベクトルがまったく同じ方向を向いている場合、値は 1 になります。反対方向を指している場合は -1 になります。その数値を照明に適用します。そのため、右上にある頂点の値は 1(完全に照らされている)に近いか等しくなり、側面にある頂点の値は 0 に近く、背面にある頂点の値は -1 になります。負の値は 0 にクランプされますが、数値を代入すると、表示されている基本的なライティングになります。

次のステップ: 頂点の位置を少し変更してみましょう。

9. 属性

ここで、属性を使用して各頂点に乱数を付加します。この数値を使用して、頂点を法線に沿って押し出します。結果として、ページを更新するたびに変化する奇妙なスパイク ボールが生成されます。まだアニメーションは表示されませんが(次のステップで表示されます)、ページを数回更新すると、ランダム化されていることがわかります。

まず、頂点シェーダーに属性を追加します。

attribute float displacement;
varying vec3 vNormal;

void main() {

vNormal = normal;

// push the displacement into the three
// slots of a 3D vector so it can be
// used in operations with other 3D
// vectors like positions and normals
vec3 newPosition = position +
                    normal *
                    vec3(displacement);

gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(newPosition,1.0);
}

実際の画面

実際にはあまり違いはありません。これは、MeshShaderMaterial で属性が設定されていないため、シェーダーが代わりにゼロ値を使用するためです。現時点ではプレースホルダのようなものです。JavaScript で MeshShaderMaterial に属性を追加すると、Three.js が自動的に 2 つを結び付けます。

また、元の属性はすべての属性と同様に読み取り専用であるため、更新された位置を新しい vec3 変数に割り当てる必要がありました。

10. MeshShaderMaterial の更新

変位を制御するために必要な属性を使用して MeshShaderMaterial を更新します。リマインダー: 属性は頂点ごとの値なので、球体の頂点ごとに 1 つの値が必要です。この場合、次のように指定します。

var attributes = {
displacement: {
    type: 'f', // a float
    value: [] // an empty array
}
};

// create the material and now
// include the attributes property
var shaderMaterial = new THREE.MeshShaderMaterial({
attributes:     attributes,
vertexShader:   $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});

// now populate the array of attributes
var vertices = sphere.geometry.vertices;
var values = attributes.displacement.value
for(var v = 0; v < vertices.length; v++) {
values.push(Math.random() * 30);
}

球体が歪んでいますが、すべての変位が GPU で発生していることがわかります。

11. アニメーションにする

これをアニメーション化すべきです。どのように実現するのか?次の 2 つの準備が必要です。

  1. 各フレームで適用する変位量をアニメーション化する uniform。正弦関数と余弦関数は -1 から 1 の範囲で実行されるため、これらを使用できます。
  2. JS のアニメーション ループ

MeshShaderMaterial と頂点シェーダーの両方に uniform を追加します。まず、頂点シェーダーは次のとおりです。

uniform float amplitude;
attribute float displacement;
varying vec3 vNormal;

void main() {

vNormal = normal;

// multiply our displacement by the
// amplitude. The amp will get animated
// so we'll have animated displacement
vec3 newPosition = position +
                    normal *
                    vec3(displacement *
                        amplitude);

gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(newPosition,1.0);
}

次に、MeshShaderMaterial を更新します。

// add a uniform for the amplitude
var uniforms = {
amplitude: {
    type: 'f', // a float
    value: 0
}
};

// create the final material
var shaderMaterial = new THREE.MeshShaderMaterial({
uniforms:       uniforms,
attributes:     attributes,
vertexShader:   $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});

これでシェーダーは完成です。しかし、右側は後退したように見えます。これは主に、振幅の値が 0 であり、それを変位に乗算しているため、変化が見られないためです。また、アニメーション ループも設定していないため、0 が他の値に変わることはありません。

JavaScript で、レンダリング呼び出しを関数にラップし、requestAnimationFrame を使用して呼び出す必要があります。また、ユニフォームの値を更新する必要があります。

var frame = 0;
function update() {

// update the amplitude based on
// the frame value
uniforms.amplitude.value = Math.sin(frame);
frame += 0.1;

renderer.render(scene, camera);

// set up the next call
requestAnimFrame(update);
}
requestAnimFrame(update);

まとめ

これで完了です。奇妙な(少しトリッピーな)脈動するようなアニメーションが表示されます。

シェーダーについては、他にも多くのトピックがあります。この入門編が皆様のお役に立てば幸いです。これで、シェーダーを見たときに理解できるようになり、独自の素晴らしいシェーダーを作成する自信がついたはずです。