見出し画像

three.js + Web Font + 頂点シェーダ芸 Vol.2 (Geometry編)

※この記事はtkmh.me上で掲載している記事 (2016.12.06 掲載) を転載、加筆・修正したものです。

---------

前回 (Vol.1 準備編)は主にシェーダを使うための準備でした。(今回Vol.2も準備の続きですが。。)

Vol.1 (準備編)
・Vol.2 (Geometry編)
Vol.3 (シェーダ編)

ここからはFloatingCharsGeometryの説明です。BufferGeometryの詳しい使い方に関してです。もうちょっとJavascriptのお話です。。

FloatingCharsGeometry.js

このクラスは正方形をたくさん持ったGeometryを生成するためのクラスです。THREE.BufferGeometryを拡張したクラスになっています。

THREE.BufferGeometryはWebGLの素のAPIに近い感覚でGeometryを生成するためのクラスです。つまり、attributeやindexのデータを自分で追加していきます。

attributeとは頂点の座標または頂点ごとに異なるデータをシェーダに渡すためのデータです。indexは、どの頂点を使って面(face)を作るを示すデータですね。

(何度も言いますが、わからない方はwgld.orgで!)

コンストラクタでは、文字数 (正方形の数)文字の幅を受け取って、継承元のコンストラクタを実行し、initメソッドを実行してるだけです。

initメソッドがこのクラスの肝となります。

・initメソッド

/**
* イニシャライズ
*/
sample.FloatingCharsGeometry.prototype.init = function() {
 // attributes用の配列を生成
 var vertices = [];      // 頂点
 var charIndices = [];   // 文字(正方形)のインデックス
 var randomValues = [];  // 頂点計算等に使用するランダム値
 var uvs = [];           // UV座標
 var indices = [];       // インデックス

 var charHeight = this.charWidth;
 var charHalfWidth = this.charWidth / 2;
 var charHalfHeight = charHeight / 2;

 // this.numCharsの数だけ正方形を生成
 for(var i = 0; i < this.numChars; i++) {

   // GLSLで使用するランダムな値
   var randomValue = [
     Math.random(),
     Math.random(),
     Math.random()
   ];

   // 頂点データを生成

   // 左上
   vertices.push(-charHalfWidth);  // x
   vertices.push(charHalfHeight);  // y
   vertices.push(0);               // z

   uvs.push(0);  // u
   uvs.push(0);  // v

   charIndices.push(i);  // 何文字目かを表すインデックス番号

   randomValues.push(randomValue[0]);  // GLSLで使用するランダムな値 (vec3になるので3つ)
   randomValues.push(randomValue[1]);  // GLSLで使用するランダムな値 (vec3になるので3つ)
   randomValues.push(randomValue[2]);  // GLSLで使用するランダムな値 (vec3になるので3つ)

   // 右上
   vertices.push(charHalfWidth);
   vertices.push(charHalfHeight);

   vertices.push(0);

   uvs.push(1);
   uvs.push(0);

   charIndices.push(i);

   randomValues.push(randomValue[0]);
   randomValues.push(randomValue[1]);
   randomValues.push(randomValue[2]);

   // 左下
   vertices.push(-charHalfWidth);
   vertices.push(-charHalfHeight);

   vertices.push(0);

   uvs.push(0);
   uvs.push(1);

   charIndices.push(i);

   randomValues.push(randomValue[0]);
   randomValues.push(randomValue[1]);
   randomValues.push(randomValue[2]);

   // 右下
   vertices.push(charHalfWidth);
   vertices.push(-charHalfHeight);

   vertices.push(0);

   uvs.push(1);
   uvs.push(1);

   charIndices.push(i);

   randomValues.push(randomValue[0]);
   randomValues.push(randomValue[1]);
   randomValues.push(randomValue[2]);

   // ポリゴンを生成するインデックスをpush (三角形ポリゴンが2枚なので6個)
   var indexOffset = i * 4;
   indices.push(indexOffset + 0);
   indices.push(indexOffset + 2);
   indices.push(indexOffset + 1);
   indices.push(indexOffset + 2);
   indices.push(indexOffset + 3);
   indices.push(indexOffset + 1);
 }

 // attributes
 this.addAttribute('position',     new THREE.BufferAttribute(new Float32Array(vertices),     3));
 this.addAttribute('randomValues', new THREE.BufferAttribute(new Float32Array(randomValues), 3));
 this.addAttribute('charIndex',    new THREE.BufferAttribute(new Uint16Array(charIndices),   1));
 this.addAttribute('uv',           new THREE.BufferAttribute(new Float32Array(uvs),          2));

 // index
 this.setIndex(new THREE.BufferAttribute(new Uint16Array(indices), 1));

 this.computeVertexNormals();
}

initメソッド内では、numChars(文字数分 = 正方形の数)forのループを回して、正方形のデータを生成しています。なので、for文の中が1つの正方形のデータを生成するコードになります。

attributeやindexのデータは、最終的にTHREE.BufferAttributeクラスをのインスタンスにし、それぞれaddAttribute、setIndexというメソッドで追加します。

その準備として、それらのデータを格納するための配列を作ります。THREE.BufferAttributeは型つき配列(Float32Array)を引数として与えるんですが、データを作るにあたり、型つき配列だと配列のように便利なメソッドが使えないので、まずはデータ分の配列を用意します。

// attributes用の配列を生成
var vertices = [];      // 頂点
var charIndices = [];   // 文字(正方形)のインデックス
var randomValues = [];  // 頂点計算等に使用するランダム値
var uvs = [];           // UV座標
var indices = [];       // インデックス

これらに、正方形の頂点データとそれに付随するデータとインデックスを追加していきます。

まずはattributeのデータを追加していくわけですが、正方形は4頂点なので、1回のループ内で「左上」「右上」「左下」「右下」の順でデータを追加していきます。

左上の頂点のデータ追加箇所を見てみましょう。

// 左上
vertices.push(-charHalfWidth);  // x
vertices.push(charHalfHeight);  // y
vertices.push(0);               // z

uvs.push(0);  // u
uvs.push(0);  // v

charIndices.push(i);  // 何文字目かを表す番号

randomValues.push(randomValue[0]);  // GLSLで使用するランダムな値 (vec3になるので3つ)
randomValues.push(randomValue[1]);  // GLSLで使用するランダムな値 (vec3になるので3つ)
randomValues.push(randomValue[2]);  // GLSLで使用するランダムな値 (vec3になるので3つ)

データの追加の仕方ですが、見てわかるように多次元配列にはせずにフラットにどんどんpushしてしまっています。これらは最終的にTHREE.BufferAttributeを生成するときに、頂点ごとに何個のデータを使うかというのを第二引数で指定します。

例えば頂点の座標であれば3次元のベクトルなので「3」となるので、とりあえず今は何も考えずにpushしていきます。

verticesは頂点座標です。x, y, z座標の順にpushしていきます。uvsはUV座標で、u, vの順にpushします。

charIndicesは、頂点が何番目の正方形に属するかを示す値です。ここにはループのインデックスiをpushしておきます。頂点、UV座標は4頂点でそれぞれ異なりますが、頂点が何番目の正方形に属するかを示す値なので、4頂点ともに同じ値(i)になります。

randomValuesは、頂点シェーダ内で各々の正方形を移動する際、正方形ごとにランダムな値が欲しいため、このようなデータを作っています。vec3にしてる意味はそんなにないです。なんとなく正方形ごとに3つくらい欲しいかなと思っただけです。
この値も正方形ごとの値としたいので、ループの一番最初に生成しておいて、4頂点で同じ値をpushしておきます。

一般化すると以下の画像のようになります。

正方形の数が増えると、以下のようなイメージになります。

全ての正方形は同じ座標(原点)にあります。これらは最終的に、頂点シェーダ内でcharIndexやrandamValuesを使用して個別の位置へ移動させます。

そしてインデックスのデータですが、これはどの頂点を使って三角形のポリゴンを作るか、というデータです。

4つ頂点のある正方形を作るには、上の図のようにポリゴンが2枚必要です。2枚のポリゴンを作るための頂点の組み合わせをindecesに追加していきます。ポリゴンのつくる頂点を選択するときは、反時計回りに頂点を選択します。

// ポリゴンを生成するインデックスをpush (三角形ポリゴンが2枚なので6個)
var indexOffset = i * 4;

indices.push(indexOffset + 0); // 左上
indices.push(indexOffset + 2); // 左下
indices.push(indexOffset + 1); // 右上

indices.push(indexOffset + 2); // 左下
indices.push(indexOffset + 3); // 右下
indices.push(indexOffset + 1); // 右上

単純に0, 2, 1と2, 3, 1を追加してしまうと、それらは1(iでいうと0)枚目の正方形の頂点の番号になってしまうので、indexOffsetを加算した値をpushします。1枚の正方形は4頂点含まれるので、i * 4という値を加算します。

そしてforループが終わったあとに、各種配列を使用してTHREE.BufferAttributeのインスタンスを作ます。

まずはattributeから。先程述べた通り、型つき配列に変換したものを第一引数とし、頂点ごとに何個データを使うかを第二引数に入れます。vec3にしたいものは3、vec2にしたいものは2, floatにしたいものは1になります。

できたインスタンスをaddAttributeメソッドで追加します。addAttributeメソッドの第一引数はシェーダで使用するattribute変数名です。

// attributes
this.addAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));  // vec3
this.addAttribute('randomValues', new THREE.BufferAttribute(new Float32Array(randomValues), 3));  // vec3
this.addAttribute('charIndex', new THREE.BufferAttribute(new Float32Array(charIndices), 1));  // float
this.addAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvs), 2));  // vec2

続いてインデックスです。インデックスもTHREE.BufferAttributeのインスタンスを作ります。第二引数は1です。

できたインスタンスをsetIndexメソッドで追加します。

// index
this.setIndex(new THREE.BufferAttribute(new Uint16Array(indices), 1));

(addAttributeもsetIndexもTHREE.BufferGeometryのメソッドです。)

最後にcomputeVertexNormalsメソッドを実行して完成です。

※このメソッドは頂点法線を計算し、attribute変数normalがシェーダで使用できるようになります。頂点法線はライトのあたり具合を計算したりするために使用しますが、今回は使用しません。

ここまできてやっとシェーダを使う準備ができました。

次はやっとシェーダ編に突入です。


サポートいただければ、レッドブルを飲んでより頑張れると思います。翼を授けてください。