こんにちは、エンジニアのオオバです。

本記事ではSD UnityちゃんをWebGLに表示させてみようと思います。

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

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

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

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

objフォーマットを解析して生WebGLでUnityちゃんを表示させる_0

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

objフォーマットを解析して生WebGLでUnityちゃんを表示させる_1

上記のオプションでOBJファイルを書き出します。

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

cube.obj · GitHub

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

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

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

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

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

objフォーマットを解析して生WebGLでUnityちゃんを表示させる_2

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

例えば頂点番号2は、2行目のv 1.000000 -1.000000 1.000000です。頂点座標を加えた状態が以下の図です。

objフォーマットを解析して生WebGLでUnityちゃんを表示させる_3

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

objフォーマットを解析して生WebGLでUnityちゃんを表示させる_4

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

objフォーマットを解析して生WebGLでUnityちゃんを表示させる_5

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

objParser(https://baobao.github.io/webgl-loadobj/)

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

まとめ

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

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

コチラに全てのソースをアップ済みです。
GitHub - baobao/webgl-loadobj

オススメ記事
参考サイト