シェーダーの概要

はじめに

前回は Three.js の概要をご紹介しました。この記事をお読みでない場合は、これを基礎としてこれから説明します。

シェーダーについてお話したいと思いますWebGL は優れており、以前にも述べたように、Three.js(およびその他のライブラリ)はこうした難点を抽象化する優れた機能を備えています。しかし、特定の効果を実現したいときや、驚くべき機能が画面にどのように現れたのかをもう少し深く掘り下げたいときもあり、シェーダーはほぼ間違いなくその方程式の要素となるでしょう。私も前回のチュートリアルで取り上げた基本的な内容から、もう少しややこしい内容に進んでもよいかもしれません。ここでは、Three.js を使用していると仮定して作業を進めます。Three.js は、シェーダーの処理に多くの労力を要します。最初に、シェーダーのコンテキストを説明します。このチュートリアルの後半では、もう少し高度な領域に入ります。その理由は、シェーダーは一見すると一般的ではなく、少し説明が必要なためです。

1. 2 つのシェーダー

WebGL では、固定パイプラインを使用できません。固定パイプラインとは、既存のものをレンダリングする手段がないことを意味します。しかし、利用できるのはプログラム可能なパイプラインです。強力ですが、理解して使用するのが難しくなります。簡単に言うと、プログラマブル パイプラインとは、頂点などを画面にレンダリングする責任をプログラマーが負うことを意味します。シェーダーはこのパイプラインの一部であり、次の 2 種類があります。

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

どちらとも言えないと思うので、それだけではまったく意味がありません。注意すべき点は、どちらも完全にグラフィック カードの GPU で実行されるということです。つまり、CPU に他の処理を任せて、できる限りのことをオフロードする必要があります。最新の GPU はシェーダーが必要とする関数に合わせて高度に最適化されているため、非常に便利です。

2. 頂点シェーダー

球のような標準的なプリミティブ シェイプを使用します。頂点でできているということですね。 頂点シェーダーには、これらの頂点の 1 つ 1 つが順番に与えられ、それらの頂点を操作することができます。頂点シェーダーがどのように機能するかは頂点シェーダーによって決まりますが、役割が 1 つあります。それは、ある時点で gl_Position(画面上の頂点の最終的な位置である 4D 浮動小数点ベクトル)と呼ばれるものを設定する必要があります。これは、2D スクリーン上に 3D 位置(x、y、z を持つ頂点)を取得する、つまり投影することに関することなので、それ自体は非常に興味深いプロセスです。幸いなことに、Three.js などを使用している場合は、負担を増やさずに gl_Position を簡単に設定できる簡単な方法があります。

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

頂点のあるオブジェクトは 2D 画面に投影しましたが 色についてはどうでしょうかテクスチャとライティングについてはどうでしょうか。フラグメント シェーダーはまさにそのためのツールです。頂点シェーダーと同様に、フラグメント シェーダーにも必須のジョブは 1 つだけです。フラグメントの最終色である gl_FragColor 変数(別の 4D 浮動小数点ベクトル)を設定または破棄する必要があります。では、フラグメントとは何でしょうか。三角形を構成する 3 つの頂点を考えてみましょう。その三角形内の各ピクセルを描画する必要があります。フラグメントは、その三角形内の各ピクセルを描画するために、この 3 つの頂点によって提供されるデータです。 このため、フラグメントは構成する頂点から補間値を受け取ります。1 つの頂点の色が赤で、その隣の頂点が青の場合、色の値は赤から紫、青に補間されます。

4. シェーダー変数

変数については、ユニフォーム、属性、可変の 3 つの宣言を作成できます。この 3 つの名前を初めて聞いたときは 今までに使用したことのある他の項目とマッチしてなかったのでとはいえ、次のように考えることができます。

  1. ユニフォームは、頂点シェーダーとフラグメント シェーダーの両方に送信され、レンダリングされるフレーム全体で同じ値になります。わかりやすい例がライトの位置です。

  2. 属性は、個々の頂点に適用する値です。属性は頂点シェーダーでのみ使用可能です。たとえば頂点ごとに異なる色を持たせたりします属性は、頂点と 1 対 1 の関係です。

  3. 可変は、フラグメント シェーダーと共有する頂点シェーダーで宣言される変数です。そのためには、頂点シェーダーとフラグメント シェーダーの両方で、同じ型と名前の可変変数を宣言します。照明の計算で使用できるため、通常は頂点の法線が使用されます。

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

ここまで、頂点シェーダーとフラグメント シェーダー、およびそれらが扱う変数の種類について説明してきました。次に、作成可能な最もシンプルなシェーダーについて確認しましょう。

5. ボンジュールノ ワールド

頂点シェーダーの 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 colour 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 を実行するということなので、ある程度の複雑さが予想されます。

実際には、さらに 2 つのプロパティ(ユニフォームと属性)を MeshShaderMaterial に追加できます。ベクトル、整数、浮動小数点数を使用できますが、前述したように、ユニフォームはフレーム全体、つまりすべての頂点で同じであるため、単一の値になる傾向があります。ただし、属性は頂点ごとの変数であるため、配列である必要があります。属性配列内の値の数とメッシュ内の頂点の数には 1 対 1 のリレーションがあります。

7. 次のステップ

少し時間をかけて、アニメーション ループ、頂点属性、ユニフォームを追加します。また、頂点シェーダーがフラグメント シェーダーにデータを送信できるように、可変変数を追加します。最終的に、ピンク色の球体が上から横に照らし合わせて点滅します。少し複雑ですが、3 つの変数の型と、それらが互いにどのように関係しているか、基礎となるジオメトリについての理解を深める一助となれば幸いです。

8. フェイクライト

フラットな色のオブジェクトにならないように色を更新します。Three.js によるライティングの処理方法を確認することは可能ですが、今回は必要以上に複雑なので、架空のものになります。Three.js に含まれている優れたシェーダーや、Chris Milk と Google が最近開発した WebGL プロジェクト(Rome)のシェーダーについても十分にご確認ください。 シェーダーに戻ります。Vertex Shader を更新して、各頂点を Fragment シェーダーに垂直方向に提供します。そのための手段はさまざまです。

// 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);
}

Fragment Shader では、同じ変数名を設定し、頂点の法線のドット積を、球体の上から右側に照らす光を表すベクトルを使用します。これにより、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 colour
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 で属性が設定されていないため、シェーダーが事実上 0 値を使用するためです。今はプレースホルダのようなものです次に、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. あの Sucker をアニメーション化

これをアニメーションにしましょう。実施方法導入すべきことは 2 つあります

  1. 各フレームに適用する変位量をアニメーション化するためのユニフォーム。正弦または余弦を使用できます
  2. JS でのアニメーション ループ

ユニフォームを MeshShaderMaterial と Vertex Shader の両方に追加しますまず、Vertex シェーダー:

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);

12. おわりに

以上で終了です。奇妙な(そして少しトリッピーな)点滅でアニメーションが表示されます。

1 つのトピックとしてシェーダーについて説明できる内容は他にもたくさんありますが、この概要がお役に立てば幸いです。シェーダーが表示されると理解し、独自の優れたシェーダーを自信を持って作成できるようになりました。