THREE.jsは初心者でも数行のスニペットをコピペするだけでWebGLを使った3Dレンダリングができる便利なライブラリです。
予め用意されている各種GeometryやMaterialを使えばテクスチャを使ったレンダリングも簡単です。
しかし触っているうちに、いろいろ疑問が生まれたり、不便だと感じてくるかもしれません。
たとえば、THREE.BoxGeometry
で、1枚の画像からそれぞれの面に対応する部分を切り取って貼り付けたい時、どうしたらいいでしょうか?
#Three.js初心者がやってしまいがちなコード
テクスチャを画像として切り貼りして回転・反転などを行って登録しています。
(以下は私がThree.jsを始めたての頃に使っていたソースコード)
See the Pen Kinoko Render Noob by Urushibara (@pneuma01) on CodePen.
一見すると、6面分のテクスチャ(マテリアル)を設定する仕組みのTHREE.BoxGeometry
の仕様的に、この使い方で正しいように思えます。
しかし、WebGLにはもともとテクスチャの切り貼りや回転・反転する機能があるので、実はメモリを無駄に消費することになっています。
#UVマッピングを理解して書き直したコード
1枚の画像のみをTextureとして登録し、その画像から部分部分を使うように書き直しています。
切り抜きと回転や反転はWebGLの機能を使っています。
See the Pen Kinoko Render by Urushibara (@pneuma01) on CodePen.
#解説
WebGLの機能を使ったテクスチャの切り抜きや回転・反転は、テクスチャ空間というものを理解する必要があります。
とは言っても難しいことはなく、仕組みが分かってしまえばこれ一つで歪みなどの変形や、座標をスライドさせるだけで行えるパラパラアニメーションにも応用できたりするので覚えておくと非常に便利です。
##面(Face)の定義
テクスチャの定義を理解するにはまず、面の定義方法を知る必要があります。
3Dレンダリングにおける、六面体の各面は2つの3角形の組み合わせによって表現されています。
図1:面のフレーム
Three.jsでは頂点の座標は Geometryオブジェクトのfaces という配列で定義されます。
faces配列は、1個の三角形毎に反時計回りに定義します。時計回りに定義すると、その面は反対側が正面であると定義されます。
「ネジを開くと手前に、締めると奥へ」と考えれば覚えやすいかもしれません。
補足情報:OpenGLなどではcullface(Face Culling)と呼ばれます。
例えば上の図の場合、[[頂点0 → 頂点3 → 頂点1], [頂点2 → 頂点1 → 頂点0], ...] という風に定義します。
ちなみに図はわかりやすいように並べていますが、始まりの頂点は 0 からではなく、1 からでも 3 からでも構わいません。
##テクスチャ情報を集める
画像を切り抜くにはまず、切り抜きたい部品の座標と大きさを調べる必要があります。
ソースコードでは以下のように定義しました。
// テクスチャマッピングデータ(topleftはくり抜きたい部品の向きを正したときの左上にあたる頂点の番号)
let tex = {
resolution: { width: 408, height: 458 },
uv: [
{ tag: "left", x: 4, y: 231, w: 54, h: 174, topleft: 1 },
{ tag: "right", x: 354, y: 231, w: 54, h: 174, topleft: 3 },
{ tag: "top", x: 57, y: 231, w: 298, h: 174, topleft: 0 },
{ tag: "bottom", x: 54, y: 0, w: 298, h: 174, topleft: 2 },
{ tag: "front", x: 57, y: 404, w: 298, h: 54, topleft: 0 },
{ tag: "back", x: 57, y: 176, w: 298, h: 54, topleft: 2 },
]
}
しかし、3Dプログラミングの世界ではテクスチャをピクセル単位で扱うことはできません。
3Dではテクスチャの配置を定義することを UVマッピングと呼びます。
(UVとはテクスチャを2次元とした場合、U軸、V軸で定義することに由来します。更に高さを加わえたUVWマッピングというものもあるそうです。)
図3:UVマッピングの座標系(面に対して倍率が100%の場合)
UVマッピングでも面の定義と同じように3つの頂点で定義します。
例えるなら、コルクボードに布を画鋲で貼り付ける感じです。
一つ一つの画鋲の位置が、頂点座標だと思えばわかりやすいでしょう。
ただし、座標系はテクスチャの画像サイズとは関係なく、画像の左下を(0,0)、右上を(1,1)とした上下反転の座標で定義することに注意が必要です。
余談ですが、ビットマップファイルのバイナリデータは伝統的に画像の一番下のラインから順に格納されています。これはボトムアップ型データと呼ばれていました。3Dでも座標が上下反転しているのは、この伝統のせいかもしれませんね。
##テクスチャの情報を頂点座標に変換する
テクスチャ画像の一部分を抜き出して使いたい場合、(0.5, 0.25), (0.5, 0.5), (1.0, 0.5) などといった指定となり、頂点の開始座標と終了座標を入れ替えることで、画像の反転も行えます。
ちなみに、マイナス座標を指定した場合や1以上が指定された場合、はみ出た部分は透明になってしまいます。
ただし、TextureオブジェクトのプロパティwrapS
、wrapT
にTHREE.RepeatWrapping
またはTHREE.MirroredRepeatWrapping
が定義されている場合は、はみ出した部分を反転させたり繰り返し表示させたりすることができます。
ソースコードではテクスチャマッピングのピクセルの座標を画像の高さと幅で割って求めています。
// テクスチャマッピングデータ(topleftはくり抜きたい部品の向きを正したときの左上にあたる頂点の番号)
let tex = {
resolution: { width: 408, height: 458 },
uv: [
{ tag: "left", x: 4, y: 231, w: 54, h: 174, topleft: 1 },
{ tag: "right", x: 354, y: 231, w: 54, h: 174, topleft: 3 },
{ tag: "top", x: 57, y: 231, w: 298, h: 174, topleft: 0 },
{ tag: "bottom", x: 54, y: 0, w: 298, h: 174, topleft: 2 },
{ tag: "front", x: 57, y: 404, w: 298, h: 54, topleft: 0 },
{ tag: "back", x: 57, y: 176, w: 298, h: 54, topleft: 2 },
]
}
let points = []
for(let i=0; i<6; i++)
{
// テクスチャデータから矩形情報を作成 ( 座標を width または height で割る )
// また、座標系はボトムアップの為、垂直座標の上下を反転させる
let rect = {
left: tex.uv[i].x / tex.resolution.width,
top: 1 - tex.uv[i].y / tex.resolution.height,
right: (tex.uv[i].x + tex.uv[i].w) / tex.resolution.width,
bottom: 1 - (tex.uv[i].y + tex.uv[i].h) / tex.resolution.height
}
// 矩形情報から頂点(UV Vertex)データを作成
// order は 画像の回転を修正する為の情報
points[i] = [
{ x: rect.left, y: rect.top, order: (tex.uv[i].topleft + 0) % 4 },
{ x: rect.right, y: rect.top, order: (tex.uv[i].topleft + 1) % 4 },
{ x: rect.right, y: rect.bottom, order: (tex.uv[i].topleft + 2) % 4 },
{ x: rect.left, y: rect.bottom, order: (tex.uv[i].topleft + 3) % 4 },
]
// ソートを使ってテクスチャの左上から順になるようにする(画像は上下反転)
// 3 _____ 2
// | /|
// | / |
// |/____|
// 0 1
points[i] = points[i].sort( (a,b)=>{ return b.order - a.order } );
}
##UVマッピングの頂点座標の順序
頂点座標の順序は面の頂点の登録順によって変わります。
THREE.BoxGeometryでは、このように面の2つの三角形の定義はバラバラで、頂点の設定順は対照的ではありません。
UVマッピングを手動で行う場合は、Faceの頂点座標と合わせる必要があります。
ソースコードでは以下のように配列をshift(ずらす)ことで解決しています。
// 頂点データから三角形データ(THREE.Vector2 の Array)を作成する関数
// point : 4点の頂点座標の array
// indices : 作成する3点の頂点のindexを指定する array
// Geometry.face[] の頂点順に従って指定する必要がある
// shift : テクスチャの向きを回転させる時は 1 ~ 3 加算する
let triangle = (point, indices, shift) => {
shift = shift ? shift: 0
indices = indices.map( v => (v + shift) % 4 );
return [
new THREE.Vector2( point[ indices[0] ].x, point[ indices[0] ].y ),
new THREE.Vector2( point[ indices[1] ].x, point[ indices[1] ].y ),
new THREE.Vector2( point[ indices[2] ].x, point[ indices[2] ].y )
]
}
反転や回転を行う場合、この頂点の指定順序を守り、座標を反転、回転させます。
##ジオメトリ(FaceVertexUvs)へUV情報を登録する
THREE.GeometryのUV情報は以下のように格納されます。
Geometry.faveVertexUvs[uv_layer][face_index][uv[face[face_index][0]], uv[face[face_index][1]], uv[face[face_index][2]]]
ちなみにuv_layerは 0がテクスチャ、1はAOマップ用となっており、テクスチャを重ねる用途には使用できないようです。
作成した三角形の頂点データを配列にして面データと同じ順序で入れます。
ソースコードでは以下のようにして登録しています。
// テクスチャ頂点情報をジオメトリへ追加(THREE.Geometry.faceVertexUvs)
geometry.faceVertexUvs[0] = [
// right
triangle( points[0], [ 1, 2, 0 ] ),
triangle( points[0], [ 2, 3, 0 ] ),
// left
triangle( points[1], [ 1, 2, 0 ] ),
triangle( points[1], [ 2, 3, 0 ] ),
// top
triangle( points[2], [ 1, 2, 0 ], 2 ),
triangle( points[2], [ 2, 3, 0 ], 2 ),
// bottom
triangle( points[3], [ 1, 2, 0 ] ),
triangle( points[3], [ 2, 3, 0 ] ),
// front
triangle( points[4], [ 1, 2, 0 ], 2 ),
triangle( points[4], [ 2, 3, 0 ], 2 ),
// back
triangle( points[5], [ 1, 2, 0 ], 2 ),
triangle( points[5], [ 2, 3, 0 ], 2 ),
];