macOSでOpenGLプログラミング (4-2. OBJファイルのモデルを読み込む)

[macOSでOpenGLプログラミングの目次に戻る](https://qiita.com/sazameki/items/d060f20f621c658046d9)

# はじめに

この記事では、3Dモデルをファイルから読み込んで表示させてみましょう。ライティングが有効になると、複雑な3Dモデルを読み込んで表示させることができるようになります。

これまでは手書きで頂点データを編集してきましたが、3Dモデルのファイルからデータを読み込めるようになると、表現力が一気に増します。手書きだと、せいぜい10個〜100個くらいの頂点データしか扱えませんが、3Dモデルから読み込むようにすると、何千・何万といった頂点データを扱えるようになります。

こうなるとこちらのもので、Maya や Blender といった DCC (Digital Content Creation)ツールを使って、思い通りのキャラクタを自分のゲームの中に表示できるようになるのです。Wavefront OBJ（拡張子 .obj）は基本的なフォーマットですから、あらゆるDCCツールがOBJファイルの書き出しに対応しています。

それでは、張り切っていきましょう。


# 1. 3Dモデルファイルを用意する

今回は「Stanford Bunny」と呼ばれる有名なウサギの3Dモデルのデータを使ってみましょう。OpenGLで3Dモデルと言えば、このウサギとティーポットが有名です。

　<img width="270" src="https://qiita-image-store.s3.amazonaws.com/0/166309/9e56cef9-9393-bf24-665f-130d72575b8e.png">　<img width="300" src="https://qiita-image-store.s3.amazonaws.com/0/166309/49a39531-24af-dd1d-c565-d94925eb9531.png">

「Stanford Bunny obj」でGoogle検索すれば、graphics.stanford.edu 以下のドメインにあるファイルが一番にヒットします。このデータは、2503個の頂点データと、それを使った4968個のポリゴンメッシュからできています。

　<img width="350" alt="stanford.png" src="https://qiita-image-store.s3.amazonaws.com/0/166309/08818ae5-ad49-004a-7717-737b47959e40.png">

OBJファイルはテキスト形式のフォーマットですので、そのままテキストとして保存して、名前を「bunny.obj」としておきましょう。

この「bunny.obj」ファイルを、Xcodeのプロジェクトの「Resources」グループにドラッグ＆ドロップで追加します。

　<img width="220" src="https://qiita-image-store.s3.amazonaws.com/0/166309/23cb9d08-6c7e-6271-96e8-9a4a3a03c7f1.png">

ファイルの追加ダイアログが表示されますので、必ず「Copy items if needed」と「Add to targets」の2箇所のチェックボックスがチェックされている状態にして、「Finish」ボタンを押してください。

　<img width="380" src="https://qiita-image-store.s3.amazonaws.com/0/166309/cd09ae01-7763-fb50-937f-8858fe5caff5.png">

以上で3Dモデルのファイルが追加できました。

　<img width="180" src="https://qiita-image-store.s3.amazonaws.com/0/166309/89669806-4e65-4b19-b24e-72b0adbb590b.png">


# 2. Wavefront OBJファイルのフォーマット

今回利用する拡張子が「.obj」のファイルは、「Wavefront OBJファイル」と呼ばれる、3Dモデルの頂点データを格納するためのファイルです。

フォーマットの詳しい解説は「[hiramine.com OBJファイルフォーマット](http://www.hiramine.com/programming/3dmodelfileformat/objfileformat.html)」にあります。どんなOBJファイルにも対応できるようにするのは先送りにして、ここでは「Stanford Bunny」を読み込むのに必要な知識だけを確認しておきたいと思います。

bunny.obj ファイルの中身を見てみましょう。次のようになっています。

```
# OBJ file format with ext .obj
# vertex count = 2503
# face count = 4968
v -3.4101800e-003 1.3031957e-001 2.1754370e-002
v -8.1719160e-002 1.5250145e-001 2.9656090e-002
...
f 1069 1647 1578
f 1058 909 939
...
```

これを見ると、このファイルは主に3種類の行から構成されていることが分かります。「#」から始まる行と、「v」から始まる行と、「f」から始まる行です。

「#」から始まる行はコメント行ですので、無視します。

「v」から始まる行には、頂点の位置座標がスペース区切りで書かれています。

「f」から始まる行には、三角メッシュポリゴンを構成する3頂点のインデックス番号が書いてあります（ただしインデックス番号は1から始まりますので、配列のインデックスとして使う場合には-1する必要があります）。


# 3. 文字列処理の関数を追加する

OBJファイルの中身を処理しやすいように、StringSupport.hpp と StringSupport.mm に文字列処理の関数を追加しましょう。

まずはStringSupport.hppに関数の宣言を追加します。今回はこの中の `Split()` 関数しか使いませんが、いずれあると便利な `StartsWith()` 関数（プリフィックス文字列のチェック）や `EndsWith()` 関数（サフィックス文字列のチェック）、そして文字列の前後から空白文字を取り除くための `Trim()` 関数も追加しておきます。

```cpp:StringSupport.hpp（追加分）
/*! 与えられた文字列strを、separatorを区切り文字として分割します。 */
std::vector<std::string> Split(const std::string& str, const std::string& separator);

/*! 与えられた文字列strを、separatorを区切り文字として分割します。（空文字挿入のオプションあり） */
std::vector<std::string> Split(const std::string& str, const std::string& separator, bool ignoreEmptyString);

/*! 文字列strが、文字列valueで始まる文字列かどうかをチェックします。 */
bool StartsWith(const std::string& str, const std::string& value);

/*! 文字列strが、文字列valueで始まる文字列かどうかをチェックします。（大文字・小文字の区別オプションあり） */
bool StartsWith(const std::string& str, const std::string& value, bool ignoreCase);

/*! 文字列strが、文字列valueで終わる文字列かどうかをチェックします。 */
bool EndsWith(const std::string& str, const std::string& value);

/*! 文字列strが、文字列valueで終わる文字列かどうかをチェックします。（大文字・小文字の区別オプションあり） */
bool EndsWith(const std::string& str, const std::string& value, bool ignoreCase);

/*! 文字列の先頭および末尾にある空白文字（タブ文字, 改行文字, 空白文字）をすべて削除します。 */
std::string Trim(const std::string& str);

/*! 文字列の先頭および末尾から、指定された文字セットをすべて削除します。 */
std::string Trim(const std::string& str, const std::string& trimChars);
```

次に、StringSupport.mmに関数の実装を追加します。

```objc++:StringSupport.mm（追加分）
std::vector<std::string> Split(const std::string& str, const std::string& separator)
{
    return Split(str, separator, false);
}

std::vector<std::string> Split(const std::string& str, const std::string& separator, bool ignoreEmptyString)
{
    std::vector<std::string> ret;

    std::string::size_type pos = 0;
    std::string::size_type length = str.length();
    if (length == 0) {
        if (!ignoreEmptyString) {
            ret.push_back("");
        }
        return ret;
    }

    while (pos < length) {
        std::string::size_type p = str.find_first_of(separator, pos);
        if (p == std::string::npos) {
            std::string::size_type len = length - pos;
            if (!ignoreEmptyString || len > 0) {
                std::string part = str.substr(pos, len);
                ret.push_back(part);
            }
            break;
        }
        std::string::size_type len = p - pos;
        if (!ignoreEmptyString || len > 0) {
            std::string part = str.substr(pos, len);
            ret.push_back(part);
        }
        pos = p + 1;
        if (pos >= length && !ignoreEmptyString) {
            ret.push_back("");
        }
    }
    return ret;
}

bool StartsWith(const std::string& str, const std::string& value)
{
    return StartsWith(str, value, true);
}

bool StartsWith(const std::string& str, const std::string& value, bool ignoreCase)
{
    auto length = value.size();
    if (str.length() < length) {
        return false;
    }

    std::string sub = str.substr(0, length);
    if (ignoreCase) {
        return (strncasecmp(sub.c_str(), value.c_str(), length) == 0);
    } else {
        return (sub == value);
    }
}

bool EndsWith(const std::string& str, const std::string& value)
{
    return EndsWith(str, value, true);
}

bool EndsWith(const std::string& str, const std::string& value, bool ignoreCase)
{
    auto length = value.size();
    if (str.length() < length) {
        return false;
    }

    std::string sub = str.substr(str.length()-length, length);
    if (ignoreCase) {
        return (strncasecmp(sub.c_str(), value.c_str(), length) == 0);
    } else {
        return (sub == value);
    }
}

std::string Trim(const std::string& str)
{
    return Trim(str, "\t\r\n ");
}

std::string Trim(const std::string& str, const std::string& trimChars)
{
    std::string::size_type left = str.find_first_not_of(trimChars);

    if (left == std::string::npos) {
        return str;
    }
    std::string::size_type right = str.find_last_not_of(trimChars);
    return str.substr(left, right - left + 1);
}
```

# 4. 頂点データを読み込む

ファイル読み込みのために、Game.hppの先頭に、C++のファイル操作APIであるfstreamのインクルード文を追加しておきましょう。また3節で追加した文字列処理の関数を使用しますので、StringSupport.hppのインクルード文も追加します。

```cpp:Game.hpp
#ifndef Game_hpp
#define Game_hpp

#include <OpenGL/OpenGL.h>
#include <OpenGL/gl3.h>
#include <GLKit/GLKMath.h>
#include <fstream>
#include <vector>
#include "Time.hpp"
#include "Input.hpp"
#include "Shader.hpp"
#include "Texture.hpp"
#include "StringSupport.hpp"
```

それでは、これまで `vector<VertexData>` 型の `data` 変数に手書きでデータを追加していたところを、3Dファイルからデータを読み込むように書き換えましょう。

```cpp:Game.cpp（一部）
Game::Game()
{
    glEnable(GL_DEPTH_TEST);

    program = new ShaderProgram("myshader.vsh", "myshader.fsh");

    // 3Dモデルの読み込み
    std::vector<GLKVector3> vertices;
    std::string modelFilepath = GetFilepath("bunny.obj");
    std::ifstream ifs(modelFilepath);
    std::string line;
    int lineCount = 0;
    while (ifs && getline(ifs, line)) {
        lineCount++;
        if (line.length() > 0 && line[0] == '#') {
            continue;
        }
        std::vector<std::string> parts = Split(line, " ");
        if (parts.size() > 0) {
            if (parts[0] == "v") {
                if (parts.size() < 4) {
                    throw GameError("Invalid obj file format (v): [line=%d] %s", lineCount, line.c_str());
                }
                float x = atof(parts[1].c_str());
                float y = atof(parts[2].c_str());
                float z = atof(parts[3].c_str());
                vertices.push_back(GLKVector3Make(x, y, z));
            } else if (parts[0] == "f") {
                if (parts.size() < 4) {
                    throw GameError("Invalid obj file format (f): [line=%d] %s", lineCount, line.c_str());
                }
                int index0 = atoi(parts[1].c_str()) - 1;
                int index1 = atoi(parts[2].c_str()) - 1;
                int index2 = atoi(parts[3].c_str()) - 1;
                if (index0 >= vertices.size() || index1 >= vertices.size() || index2 >= vertices.size()) {
                    throw GameError("Invalid index value (f): [line=%d] %s", lineCount, line.c_str());
                }
                GLKVector3& v0 = vertices[index0];
                GLKVector3& v1 = vertices[index1];
                GLKVector3& v2 = vertices[index2];
                GLKVector3 cross = GLKVector3CrossProduct(GLKVector3Subtract(v1, v0),
                                                          GLKVector3Subtract(v2, v0));
                GLKVector3 normal = GLKVector3Normalize(cross);
                GLKVector4 color = GLKVector4Make(1.0f, 1.0f, 1.0f, 1.0f);
                data.push_back( VertexData{ v0, normal, color } );
                data.push_back( VertexData{ v1, normal, color } );
                data.push_back( VertexData{ v2, normal, color } );
            }
        }
    }
    // 以下、続く
}
```

その他の箇所は変更していません。インデックス・リストの作成方法や、VBO・VAO・IBO の作成のためのコードはこれまでとまったく同じです。

それでは実行してみましょう。次のように Stanford Bunny が表示されれば成功です。

　<img width="350" src="https://qiita-image-store.s3.amazonaws.com/0/166309/1b970e4c-4dd6-5773-f353-9bc441f17c11.png">
　　ここまでのプロジェクト：[MyGLGame_step4-2.zip](http://sazameki.jp/temp/MyGLGame_step4-2.zip)


# 5. 3Dモデルを読み込むコードの解説

（※ この節はあとでより詳しく説明を書き直しますが、まずは各箇所にざっくりとコメントをつけておきます（1月7日 15:00 現在）。）

## 5-1. ファイル読み込みはC++標準の方法で

ファイルパスを取得して、ifstreamクラスのオブジェクトを作る。

```cpp
std::string modelFilepath = GetFilepath("bunny.obj");
std::ifstream ifs(modelFilepath);
```

1行ずつ読み込むためのifstreamクラスの使い方。読み込みに問題があった時に、問題の箇所を特定しやすくするために、`lineCount`変数を用意して、何行目を処理しているか分かるようにしておくのが重要（パーサなどを作る際の基本）。

```cpp
int lineCount = 0;
while (ifs && getline(ifs, line)) {
    lineCount++;
}
```

コメント行を無視する処理

```cpp
if (line.length() > 0 && line[0] == '#') {
    continue;
}
```

空白文字で切り分ける処理。

```cpp
std::vector<std::string> parts = Split(line, " ");
if (parts.size() > 0) {
    if (parts[0] == "v") {
        ...
    } else if (parts[0] == "f") {
        ...
    }
}
```

頂点データの処理。指数表現になっている文字列も、一般的な小数点表現の文字列も、`atof()` 関数で float 値に変換できる。空白文字で区切った要素の個数を確認し、問題があったら例外をスローしてエラーを報告する。X座標, Y座標, Z座標を変換したら、頂点座標だけを格納しておくためのvectorに`push_back()`で追加する。

```cpp
if (parts[0] == "v") {
    if (parts.size() < 4) {
        throw GameError("Invalid obj file format (v): [line=%d] %s", lineCount, line.c_str());
    }
    float x = atof(parts[1].c_str());
    float y = atof(parts[2].c_str());
    float z = atof(parts[3].c_str());
    vertices.push_back(GLKVector3Make(x, y, z));
```

三角ポリゴンメッシュのデータ読み込み。頂点データと同様に要素の個数を確認して、問題があったら例外をスローする。`atoi()` 関数で文字列からint型の数値に変換し、OBJファイルではインデックス番号が1始まりなのを、配列に合わせて0始まりになるようにする。こうして取得したインデックスが、頂点データの個数を超えないことを必ずチェックする。今回使用する Stanford Bunny のOBJファイルには法線ベクトルのデータが含まれていないので、3頂点の座標を元に、外積を使って法線ベクトルを計算する。

```cpp
} else if (parts[0] == "f") {
    if (parts.size() < 4) {
        throw GameError("Invalid obj file format (f): [line=%d] %s", lineCount, line.c_str());
    }
    int index0 = atoi(parts[1].c_str()) - 1;
    int index1 = atoi(parts[2].c_str()) - 1;
    int index2 = atoi(parts[3].c_str()) - 1;
    if (index0 >= vertices.size() || index1 >= vertices.size() || index2 >= vertices.size()) {
        throw GameError("Invalid index value (f): [line=%d] %s", lineCount, line.c_str());
    }
    GLKVector3& v0 = vertices[index0];
    GLKVector3& v1 = vertices[index1];
    GLKVector3& v2 = vertices[index2];
    GLKVector3 cross = GLKVector3CrossProduct(GLKVector3Subtract(v1, v0),
                                              GLKVector3Subtract(v2, v0));
    GLKVector3 normal = GLKVector3Normalize(cross);
    GLKVector4 color = GLKVector4Make(1.0f, 0.75f, 0.0f, 1.0f);
    data.push_back( VertexData{ v0, normal, color } );
    data.push_back( VertexData{ v1, normal, color } );
    data.push_back( VertexData{ v2, normal, color } );
}
```


# 6. まとめ

この記事では、3DモデルのファイルフォーマットであるOBJファイルを読み込めるようにし、これまでとは段違いに複雑な本格的な3Dデータを画面に表示できるようにしました。

ただし、今回は比較的単純なOBJファイルである Stanford Bunny を読み込めるようにしただけなので、あらゆるOBJファイルを読み込めるようにするには、もう少し手直しが必要です。今後の記事では、それも修正していきましょう。

----
[macOSでOpenGLプログラミングの目次に戻る](https://qiita.com/sazameki/items/d060f20f621c658046d9)