渋谷ほととぎす通信

完全趣味でやってるUnityメモ。説明できないところを説明できるようにするための個人ブログ。昨日の自分より少しでも大きくなれるように。。。 ※所属団体とは一切関係がありません

生WebGLでUnityちゃんを表示させる


f:id:esakun:20180322010924p:plain:w450

今回もWebGL周りの続きです。
本記事ではSD UnityちゃんをWebGLに表示させてみようと思います。

SD UnityちゃんはFBXフォーマットで提供させているわけですが、FBXをパースするのは時間がかかりそうだったので、今回はシンプルにOBJフォーマットに変換し、テクスチャ抜きで読み込むことにしました。(後日テクスチャ貼ってみます)
※勉強のステップはできるだけ段差を小さくすることが、モチベ観点から大事だと思っている

と、その前に、まずはOBJフォーマットのパーサーを作ります。

今回欲しいデータは、頂点座標配列頂点毎の法線ベクトル配列インデックス配列のこの3種類ですので、これらを取得する最低限のパーサーをJavaScriptで作ってみたいと思います。

まずはシンプルな形状でテストしたいので、立方体のOBJファイルをBlenderを使って用意します。

f:id:esakun:20180320095844p:plain

File -> Export -> Wavefront(.obj)を選択します。

f:id:esakun:20180320100018p:plain
上記のオプションでOBJファイルを書き出します。

では、とりえあずOBJファイルの中身を見てみます。

シンプルなテキストファイルです。

これらの要素が何か調べていくと、v 行は頂点座標、vn 行が頂点の法線ベクトル、f 行は頂点毎の情報が付与されたインデックス配列ということが分かりました。

行の先頭キーワード 内容
v 頂点座標
vn 法線ベクトル
f 頂点毎の情報が付与されたインデックス配列

f行が重要で、頂点番号// 法線ベクトル番号という内容の配列で三角形メッシュの面情報を表しています。

三角形の点をp1,p2,p3とした場合、インデックス配列の1番目f 2//1 4//1 1//1は、以下の図のようになります。

f:id:esakun:20180321144421p:plain:w300

頂点番号2, 4, 1はv行の行番号になります。

例えば頂点番号2は、2行目のv 1.000000 -1.000000 1.000000です。頂点座標を加えた状態が以下の図です。
f:id:esakun:20180321145546p:plain:w400

最後に法線ベクトルです。
法線ベクトル番号はvn行の行番号になります。それを反映した図が以下です。

f:id:esakun:20180321151841p:plain:w440

f行を見ていくと1頂点に対して、複数のベクトル番号が指定されています。各面毎の法線ベクトルなので、1つの頂点が所属する面の法線を全て加算すれば頂点毎の法線ベクトルが算出できます。

f:id:esakun:20180321155413p:plain:w400

頂点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
    };
}

objParser.js

コチラが実際の挙動です。
※動作確認はChrome、Safari
https://baobao.github.io/webgl-loadobj/

SD UnityちゃんをOBJファイルに書き出し直してロードさせてみました。

まとめ

今回の対応で、キューブやトーラスなどのプリミティブ以外のメッシュが表示できるようになったので、WebGLの勉強がモチベーション的に捗りそうです。

今後はテクスチャ、スキンメッシュに対応していきたいと思います。 また、今回のOBJパーサーはかなり限定的な仕様で作っているので、もう少し汎用的な形で最終着地させていくかもしれません。

コチラに全てのソースをアップ済みです。

参考