Three.jsに興味を持ったので、直方体を作るBoxGeometryがどのような処理で実現しているかソースコードを読んでみました。
リビジョンは r125 です。
基本的な変数の代入処理
constructor( width = 1, height = 1, depth = 1, widthSegments = 1, heightSegments = 1, depthSegments = 1 ) {
super();
this.type = 'BoxGeometry';
this.parameters = {
width: width,
height: height,
depth: depth,
widthSegments: widthSegments,
heightSegments: heightSegments,
depthSegments: depthSegments
};
const scope = this;
見たままですが、幅や高さなどの必要な情報をメンバ変数に代入しています。
面の分割数の設定
// segments
widthSegments = Math.floor( widthSegments );
heightSegments = Math.floor( heightSegments );
depthSegments = Math.floor( depthSegments );
分割数の引数に整数でない数値を指定した場合は小数点以下を切り捨てます。
座標情報等を保持する変数
インデックス(=三角形を作る頂点の組み合わせ?)、頂点、頂点の向き、UV座標を保持する配列の準備
// buffers
const indices = [];
const vertices = [];
const normals = [];
const uvs = [];
頂点の合計と、グループ化する際の起点を保持する変数
// helper variables
let numberOfVertices = 0;
let groupStart = 0;
各面の頂点等の座標情報を定義した配列に追加する
詳細は後で書きますが、前述の座標情報用の変数に定義しているようです。
// build each side of the box geometry
buildPlane( 'z', 'y', 'x', - 1, - 1, depth, height, width, depthSegments, heightSegments, 0 ); // px
buildPlane( 'z', 'y', 'x', 1, - 1, depth, height, - width, depthSegments, heightSegments, 1 ); // nx
buildPlane( 'x', 'z', 'y', 1, 1, width, depth, height, widthSegments, depthSegments, 2 ); // py
buildPlane( 'x', 'z', 'y', 1, - 1, width, depth, - height, widthSegments, depthSegments, 3 ); // ny
buildPlane( 'x', 'y', 'z', 1, - 1, width, height, depth, widthSegments, heightSegments, 4 ); // pz
buildPlane( 'x', 'y', 'z', - 1, - 1, width, height, - depth, widthSegments, heightSegments, 5 ); // nz
ジオメトリの作成
// build geometry
this.setIndex( indices );
this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
本題
buildPlaneという関数がコンストラクタ内に定義されており、この関数が角頂点の座標や、インデックスなどの定義を行っていました。
BoxGeometryを作成する根幹となる処理のようです。
function buildPlane( u, v, w, udir, vdir, width, height, depth, gridX, gridY, materialIndex ) {
それぞれの引数が何を受け取るのか、素人にはわかりづらいのでコメントでまとめると下記の通りかなと
/**
* @param {string} u 横として扱う座標軸の指定
* @param {string} v 縦として扱う座標軸の指定
* @param {string} w 奥行きとして扱う座標軸の指定
* @param {number} udir 横の向き 1(右) or -1(左)
* @param {number} vdir 縦の向き 1(上) or -1(下)
* @param {number} width 横幅
* @param {number} height 縦幅
* @param {number} depth 奥行き幅
* @param {number} gridX 横軸の分割数
* @param {number} gridY 縦軸の分割数
* @param {number} materialIndex 面の番号?
*/
function buildPlane( u, v, w, udir, vdir, width, height, depth, gridX, gridY, materialIndex ) {
変数の定義
const segmentWidth = width / gridX;
const segmentHeight = height / gridY;
const widthHalf = width / 2;
const heightHalf = height / 2;
const depthHalf = depth / 2;
const gridX1 = gridX + 1;
const gridY1 = gridY + 1;
let vertexCounter = 0;
let groupCount = 0;
const vector = new Vector3();
各変数は下記コメントの通りの値が入っています
const segmentWidth = width / gridX; // 分割した1つあたりの横幅を計算した値
const segmentHeight = height / gridY; // 分割した1つあたりの縦幅を計算した値
const widthHalf = width / 2; // 横幅の中央を計算した値
const heightHalf = height / 2; // 縦幅の中央を計算した値
const depthHalf = depth / 2; // 奥行き幅の中央を計算した値
// 中央値を計算するのは 指定した幅から-座標と+座標の値を算出するため
const gridX1 = gridX + 1; // 縦軸の辺の数を計算(分割数に+1)した値
const gridY1 = gridY + 1; // 横軸の辺の数を計算(分割数に+1)した値
let vertexCounter = 0; // 頂点の数
let groupCount = 0; // インデックスの数
const vector = new Vector3(); // 三次元ベクトルの準備。使い方としてはMapの代用に見えます
頂点、法線ベクトル、UVの計算
// generate vertices, normals and uvs
for ( let iy = 0; iy < gridY1; iy ++ ) {
const y = iy * segmentHeight - heightHalf;
for ( let ix = 0; ix < gridX1; ix ++ ) {
const x = ix * segmentWidth - widthHalf;
// set values to correct vector component
vector[ u ] = x * udir;
vector[ v ] = y * vdir;
vector[ w ] = depthHalf;
// now apply vector to vertex buffer
vertices.push( vector.x, vector.y, vector.z );
// set values to correct vector component
vector[ u ] = 0;
vector[ v ] = 0;
vector[ w ] = depth > 0 ? 1 : - 1;
// now apply vector to normal buffer
normals.push( vector.x, vector.y, vector.z );
// uvs
uvs.push( ix / gridX );
uvs.push( 1 - ( iy / gridY ) );
// counters
vertexCounter += 1;
}
}
長いので少しずつ絞り込んで意味を考えます
もう少し詳細
for ( let iy = 0; iy < gridY1; iy ++ ) {
const y = iy * segmentHeight - heightHalf;
頂点を計算しないといけないので、横軸の辺の数だけループを回し、縦軸の頂点座標を計算しています。
for ( let ix = 0; ix < gridX1; ix ++ ) {
const x = ix * segmentWidth - widthHalf;
頂点を計算しないといけないので、縦軸の辺の数だけループで回して、横軸の頂点座標を計算しています。
// set values to correct vector component
vector[ u ] = x * udir;
vector[ v ] = y * vdir;
vector[ w ] = depthHalf;
// now apply vector to vertex buffer
vertices.push( vector.x, vector.y, vector.z );
角頂点の座標を計算しています。
vector[ u ] は横軸の座標
vector[ v ] は縦軸の座標
vector[ w ] は奥行き軸の座標。基本的にはXY座標を計算する関数なので、常に同じ軸の座標が指定されるようになっている
※変数u,v,wには各面を正面にしたときの横、縦、奥行きの座標軸が定義されています。
// set values to correct vector component
vector[ u ] = 0;
vector[ v ] = 0;
vector[ w ] = depth > 0 ? 1 : - 1;
// now apply vector to normal buffer
normals.push( vector.x, vector.y, vector.z );
各頂点の法線ベクトルを計算をしています。
言葉が難しいのですが、関数自体が対象の面を正面にしたときを前提にしているので、縦横は無視して奥行きの指定によって分岐させています。
※変数u,v,wには各面を正面にしたときの横、縦、奥行きの座標軸が定義されています。
// uvs
uvs.push( ix / gridX );
uvs.push( 1 - ( iy / gridY ) );
UV座標の計算をしています。
U座標には分割数に応じて0-1の間を等分した値が入っています。V座標には1からU座標を引いた値が入ります
(何故そうなるのかはまだわかっていません・・・)
// counters
vertexCounter += 1;
頂点の数をカウントしています。
関数を呼び出すごとに各面の頂点の数が加算されていきます。
各面を構成するインデックスを計算
// indices
// 1. you need three indices to draw a single face
// 2. a single segment consists of two faces
// 3. so we need to generate six (2*3) indices per segment
for ( let iy = 0; iy < gridY; iy ++ ) {
for ( let ix = 0; ix < gridX; ix ++ ) {
const a = numberOfVertices + ix + gridX1 * iy;
const b = numberOfVertices + ix + gridX1 * ( iy + 1 );
const c = numberOfVertices + ( ix + 1 ) + gridX1 * ( iy + 1 );
const d = numberOfVertices + ( ix + 1 ) + gridX1 * iy;
// faces
indices.push( a, b, d );
indices.push( b, c, d );
// increase counter
groupCount += 6;
}
}
こちらも長いので少しずつ絞り込んで考えます。
for ( let iy = 0; iy < gridY; iy ++ ) {
for ( let ix = 0; ix < gridX; ix ++ ) {
インデックスは頂点ごとではなく、面ごとなので、分割数でループさせます。
const a = numberOfVertices + ix + gridX1 * iy;
const b = numberOfVertices + ix + gridX1 * ( iy + 1 );
const c = numberOfVertices + ( ix + 1 ) + gridX1 * ( iy + 1 );
const d = numberOfVertices + ( ix + 1 ) + gridX1 * iy;
numberOfVerticesには各面の頂点の合計値が加算されている変数で、
面ごとの頂点を算出する起点として使われているようです。
// faces
indices.push( a, b, d );
indices.push( b, c, d );
三角形を構成する頂点ごとにまとめてindeces変数に追加しています。
面を作るために三角形が二つ必要なので、ループ1回ごとに2つ追加されていきます。
// increase counter
groupCount += 6;
面を構成している三角形の頂点の数を加算しています。
// add a group to the geometry. this will ensure multi material support
scope.addGroup( groupStart, groupCount, materialIndex );
面ごとの頂点をグループとしてまとめています
// calculate new start value for groups
groupStart += groupCount;
各面で使用している頂点の合計値を加算して、次のグループが配列のどのindexから始まるかの基準としています。
// update total number of vertices
numberOfVertices += vertexCounter;
頂点の数を加算して、面ごとの頂点がどこから始まるかの基準としています。
まとめ
言葉がまとめられていない気がしますが、少しずつ理解を深めていければと思います。