こんにちは、エンジニアのオオバです。
本記事ではSD UnityちゃんをWebGLに表示させてみようと思います。
SD UnityちゃんはFBXフォーマットで提供させているわけですが、FBXをパースするのは時間がかかりそうだったので、今回はシンプルにOBJフォーマットに変換し、テクスチャ抜きで読み込むことにしました。(後日テクスチャ貼ってみます)
と、その前に、まずはOBJフォーマットのパーサーを作ります。
今回欲しいデータは、頂点座標配列
、頂点毎の法線ベクトル配列
、インデックス配列
のこの3種類ですので、これらを取得する最低限のパーサーをJavaScriptで作ってみたいと思います。
まずはシンプルな形状でテストしたいので、立方体のOBJファイルをBlenderを使って用意します。
File -> Export -> Wavefront(.obj)
を選択します。
上記のオプションでOBJファイルを書き出します。
では、とりえあずOBJファイルの中身を見てみます。
シンプルなテキストファイルです。
これらの要素が何か調べていくと、v 行
は頂点座標、vn 行
が頂点の法線ベクトル、f 行
は頂点毎の情報が付与されたインデックス配列ということが分かりました。
行の先頭キーワード | 内容 |
---|---|
v | 頂点座標 |
vn | 法線ベクトル |
f | 頂点毎の情報が付与されたインデックス配列 |
f行が重要で、頂点番号
// 法線ベクトル番号
という内容の配列で三角形メッシュの面情報を表しています。
三角形の点をp1,p2,p3とした場合、インデックス配列の1番目f 2//1 4//1 1//1
は、以下の図のようになります。
頂点番号2, 4, 1はv行
の行番号になります。
例えば頂点番号2は、2行目のv 1.000000 -1.000000 1.000000
です。頂点座標を加えた状態が以下の図です。
最後に法線ベクトルです。
法線ベクトル番号はvn行
の行番号になります。それを反映した図が以下です。
f行を見ていくと1頂点に対して、複数のベクトル番号が指定されています。各面毎の法線ベクトルなので、1つの頂点が所属する面の法線を全て加算すれば頂点毎の法線ベクトルが算出できます。
頂点p1が3面所属していて、その法線ベクトルv1, v2, v3とすると、頂点p1の法線ベクトルvqはv1とv2とv3を加算した値です。
※最終的にはvqを正規化した値が法線ベクトルです
これらの情報を踏まえた必要最低限のOBJパーサーを作るとこうなりました。
※OBJフォーマットの注意点として、インデックスが1始まりなので、プログラム上では1引くことを忘れずに。
function parse(text)
{
// 頂点配列
var pos = [];
// 法線配列
var normal = [];
// 頂点Index配列
var vertexIndexList = [];
// 法線頂点Index配列
var normalIndexList = [];
// objファイルテキストを行単位で格納した配列
var textArray = text.split(/\r\n|\r|\n/);
// 法線Vector3配列
var normalVector3List = [];
var indexDataList = [];
for (var i = 0; i < textArray.length; i++)
{
var line = textArray[i];
if (line.indexOf('v ') === 0)
{
// vertex
var tmp = line.split(' ');
// 0番目は `v`なので無視
pos.push(tmp[1]);
pos.push(tmp[2]);
pos.push(tmp[3]);
}
else if (line.indexOf('vn ') === 0)
{
// normal
var tmp = line.split(' ');
// 0番目は `vn`なので無視
normalVector3List.push({
"x":tmp[1], "y":tmp[2],"z":tmp[3]
});
}
else if (line.indexOf('f ') === 0)
{
// index
var tmp = line.split(' ');
// 0番目は `f `なので無視
var p0 = tmp[1].split("/");
var p1 = tmp[2].split("/");
var p2 = tmp[3].split("/");
indexDataList.push({
"v":p0[0] - 1,
"n":p0[2] - 1
});
indexDataList.push({
"v":p1[0] - 1,
"n":p1[2] - 1
});
indexDataList.push({
"v":p2[0] - 1,
"n":p2[2] - 1
});
vertexIndexList.push(p0[0] - 1);
vertexIndexList.push(p1[0] - 1);
vertexIndexList.push(p2[0] - 1);
}
}
// 面法線情報を頂点法線に変換する
var vertCnt = pos.length / 3;
for(var vertexIndexNum = 0; vertexIndexNum < vertCnt; vertexIndexNum++)
{
var normalIndexList = [];
for (var i = 0; i < indexDataList.length; i++)
{
var indexData = indexDataList[i];
if (indexData["v"] == vertexIndexNum)
{
var normalIndex = indexData["n"];
if (normalIndexList.indexOf(normalIndex) < 0)
{
// 法線Index配列
normalIndexList.push(normalIndex);
}
}
}
var rx = 0;
var ry = 0;
var rz = 0;
for (var i = 0; i < normalIndexList.length; i++)
{
var normalIndex = normalIndexList[i];
var normalVector = normalVector3List[normalIndex];
rx += parseFloat(normalVector["x"]);
ry += parseFloat(normalVector["y"]);
rz += parseFloat(normalVector["z"]);
}
// 正規化
var distance = Math.sqrt(rx*rx + ry*ry + rz*rz);
normal.push(rx/distance);
normal.push(ry/distance);
normal.push(rz/distance);
}
return {
"position": pos,
"index": vertexIndexList,
"normal": normal
};
}
コチラが実際の挙動です。
※動作確認はChrome、Safari
objParser(https://baobao.github.io/webgl-loadobj/)
SD UnityちゃんをOBJファイルに書き出し直してロードさせてみました。
まとめ
今回の対応で、キューブやトーラスなどのプリミティブ以外のメッシュが表示できるようになったので、WebGLの勉強がモチベーション的に捗りそうです。
今後はテクスチャ、スキンメッシュに対応していきたいと思います。
また、今回のOBJパーサーはかなり限定的な仕様で作っているので、もう少し汎用的な形で最終着地させていくかもしれません。
コチラに全てのソースをアップ済みです。
GitHub - baobao/webgl-loadobj
この記事が気に入ったらフォローしよう