OpenGLでobjを読み込み、単色塗りつぶしで表示してみる

はじめに

この記事はシリーズ記事です。目次はこちら。

この記事ではobjファイルを読み込み単色塗りつぶしで表示するところまでを行います。

screenshot 0

プログラムの変更点

前回からの変更点を次に述べます。

Meshクラスの変更

Meshクラスを次のように変更します。

まずはコンストラクタ。

mesh.h
-  Mesh(const std::vector<glm::vec3>& vertices,
-       const std::vector<glm::vec3>& colors);
+  Mesh(const std::vector<glm::vec3>& vertices);
mesh.cpp
-Mesh::Mesh(const std::vector<glm::vec3>& vertices,
-           const std::vector<glm::vec3>& colors)
+Mesh::Mesh(const std::vector<glm::vec3>& vertices)
    : size_(vertices.size()) {
  glGenVertexArrays(1, &vao_);
  glBindVertexArray(vao_);

  glGenBuffers(1, &vertices_vbo_);
  glBindBuffer(GL_ARRAY_BUFFER, vertices_vbo_);
  glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(glm::vec3),
               &vertices[0], GL_STATIC_DRAW);
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, static_cast<void*>(0));

-  glGenBuffers(1, &colors_vbo_);
-  glBindBuffer(GL_ARRAY_BUFFER, colors_vbo_);
-  glBufferData(GL_ARRAY_BUFFER, colors.size() * sizeof(glm::vec3), &colors[0],
-               GL_STATIC_DRAW);
-  glEnableVertexAttribArray(1);
-  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, static_cast<void*>(0));
-
  glBindBuffer(GL_ARRAY_BUFFER, 0);
  glBindVertexArray(0);
}

頂点カラーは今回は使わないので削除しました。

必要なくなったCreateTriangleMeshは削除します。

mesh.h
-  static std::unique_ptr<Mesh> CreateTriangleMesh();

ヘッダを追加でいくつかインクルードします。

+#include <fstream>
#include <glm/glm.hpp>
#include <iostream>
#include <memory>
+#include <sstream>
+#include <string>
#include <vector>

LoadObjMeshという静的メンバ関数を作成します。

mesh.h
  /**
   * @brief objを読み込む静的メンバ関数
   * @param file 読み込むobjファイルのパス
   * @return 読み込んだMesh
   */
  static std::shared_ptr<Mesh> LoadObjMesh(const std::string file);

...

  /**
   * @brief 文字列を指定した区切り文字で分割する静的メンバ関数
   * @param s 分割する文字列
   * @param delim 分割に使う区切り文字
   * @return 分割された文字列
   */
  static std::vector<std::string> SplitString(const std::string& s, char delim);
mesh.cpp
std::vector<std::string> Mesh::SplitString(const std::string& s, char delim) {
  std::vector<std::string> elems(0);
  std::stringstream ss;
  ss.str(s);
  std::string item;
  while (std::getline(ss, item, delim)) {
    elems.push_back(item);
  }
  return elems;
}

std::shared_ptr<Mesh> Mesh::LoadObjMesh(const std::string file) {
  std::vector<unsigned int> vertex_indices;
  std::vector<glm::vec3> tmp_vertices;
  std::vector<glm::vec3> vertices;

  std::ifstream ifs(file);
  std::string line;
  if (ifs.fail()) {
    std::cerr << "Can't open obj file: " << file << std::endl;
    return nullptr;
  }
  while (getline(ifs, line)) {
    auto col = SplitString(line, ' ');

    if (col[0] == "v") {
      tmp_vertices.emplace_back(std::stof(col[1]), std::stof(col[2]),
                                std::stof(col[3]));
    } else if (col[0] == "f") {
      auto v1 = SplitString(col[1], '/');
      auto v2 = SplitString(col[2], '/');
      auto v3 = SplitString(col[3], '/');
      vertex_indices.emplace_back(std::stoi(v1[0]));
      vertex_indices.emplace_back(std::stoi(v2[0]));
      vertex_indices.emplace_back(std::stoi(v3[0]));
    }
  }

  for (unsigned int i = 0; i < vertex_indices.size(); i++) {
    unsigned int vertexIndex = vertex_indices[i];
    vertices.emplace_back(tmp_vertices[vertexIndex - 1]);
  }

  return std::make_shared<Mesh>(vertices);
}

objファイルのうち頂点座標と面の情報を読み込んでMeshを構築しています。

MeshEntityクラスの作成

Meshを保持し、Model行列を保持するMeshEntityクラスを作成します。

mesh_entity.h
#ifndef OPENGL_PBR_MAP_MESH_ENTITY_H_
#define OPENGL_PBR_MAP_MESH_ENTITY_H_

#include <glm/glm.hpp>
#include <glm/gtx/euler_angles.hpp>
#include <memory>

#include "mesh.h"

namespace game {

/**
 * @brief MeshとModel行列を保持するクラス
 */
class MeshEntity {
 public:
  const std::shared_ptr<const Mesh> mesh_;

  /**
   * @brief シーン上の位置を返す
   * @return シーン上の位置
   */
  const glm::vec3 GetPosition() const;

  /**
   * @brief シーン上の位置を設定する
   * @param position 新しい位置
   *
   * 内部で保持するModel行列の再計算が行われます。
   */
  void SetPosition(const glm::vec3 position);

  /**
   * @brief このEntityの回転のオイラー角を取得する
   * @return このEntityの回転のオイラー角
   *
   * オイラー角はYXZの順です。
   */
  const glm::vec3 GetRotation() const;

  /**
   * @brief このEntityの回転のオイラー角を設定する
   * @param rotation 新しい回転のオイラー角
   *
   * オイラー角はYXZの順です。
   * 内部で保持するModel行列の再計算が行われます。
   */
  void SetRotation(const glm::vec3 rotation);

  /**
   * @brief このEntityのスケールを取得する
   * @return このEntityのスケール
   */
  const glm::vec3 GetScale() const;

  /**
   * @brief このEntityのスケールを設定する
   * @param scale 新しいスケール
   *
   * 内部で保持するModel行列の再計算が行われます。
   */
  void SetScale(const glm::vec3 scale);

  /**
   * @brief Model行列を取得する
   * @return Model行列
   */
  const glm::mat4 GetModelMatrix() const;

  /**
   * @brief コンストラクタ
   * @param mesh Meshのshared_ptr
   * @param position Entityの位置
   * @param rotation Entityの回転のオイラー角
   * @param scale Entityの各軸のスケール
   *
   * オイラー角はYXZの順です。
   */
  MeshEntity(const std::shared_ptr<const Mesh> mesh, const glm::vec3 position,
             const glm::vec3 rotation, const glm::vec3 scale);

 private:
  glm::vec3 position_;
  glm::vec3 rotation_;
  glm::vec3 scale_;
  glm::mat4 model_matrix_;

  /**
   * @brief Model行列を再計算する
   */
  void RecaluculateModelMatrix();
};

}  // namespace game

#endif  // OPENGL_PBR_MAP_MESH_ENTITY_H_
mesh_entity.cpp
#include "mesh_entity.h"

namespace game {

const glm::vec3 MeshEntity::GetPosition() const { return position_; }

void MeshEntity::SetPosition(const glm::vec3 position) {
  position_ = position;
  RecaluculateModelMatrix();
}

const glm::vec3 MeshEntity::GetRotation() const { return rotation_; }

void MeshEntity::SetRotation(const glm::vec3 rotation) {
  rotation_ = rotation;
  RecaluculateModelMatrix();
}

const glm::vec3 MeshEntity::GetScale() const { return scale_; }

void MeshEntity::SetScale(const glm::vec3 scale) {
  scale_ = scale;
  RecaluculateModelMatrix();
}

const glm::mat4 MeshEntity::GetModelMatrix() const { return model_matrix_; }

MeshEntity::MeshEntity(const std::shared_ptr<const Mesh> mesh,
                       const glm::vec3 position, const glm::vec3 rotation,
                       const glm::vec3 scale)
    : mesh_(mesh),
      position_(position),
      rotation_(rotation),
      scale_(scale),
      model_matrix_() {
  RecaluculateModelMatrix();
}

void MeshEntity::RecaluculateModelMatrix() {
  auto scale = glm::scale(glm::mat4(1), scale_);
  auto rotation = glm::eulerAngleYXZ(rotation_.y, rotation_.x, rotation_.z);
  auto translate = glm::translate(glm::mat4(1), position_);
  model_matrix_ = translate * rotation * scale;
}

}  // namespace game

Meshのshared_ptrを保持しています。

mesh_entity.h
class MeshEntity {
 public:
  const std::shared_ptr<const Mesh> mesh_;
  ...

平行移動成分をglm::vec3で保持しています。 オイラー角のglm::vec3でローテーション情報を保持しています。 拡大縮小成分もglm::vec3で保持しています。

mesh_entity.h
...
 private:
  glm::vec3 position_;  glm::vec3 rotation_;  glm::vec3 scale_;  glm::mat4 model_matrix_;
...

これらの情報から計算されるModel行列をglm::mat4で保持しています。

mesh_entity.h
...
 private:
  glm::vec3 position_;
  glm::vec3 rotation_;
  glm::vec3 scale_;
  glm::mat4 model_matrix_;...

それぞれの情報のGetterとSetterを用意しています。

mesh_entity.h
...

  /**
   * @brief シーン上の位置を返す
   * @return シーン上の位置
   */
  const glm::vec3 GetPosition() const;

  /**
   * @brief シーン上の位置を設定する
   * @param position 新しい位置
   *
   * 内部で保持するModel行列の再計算が行われます。
   */
  void SetPosition(const glm::vec3 position);

  /**
   * @brief このEntityの回転のオイラー角を取得する
   * @return このEntityの回転のオイラー角
   *
   * オイラー角はYXZの順です。
   */
  const glm::vec3 GetRotation() const;

  /**
   * @brief このEntityの回転のオイラー角を設定する
   * @param rotation 新しい回転のオイラー角
   *
   * オイラー角はYXZの順です。
   * 内部で保持するModel行列の再計算が行われます。
   */
  void SetRotation(const glm::vec3 rotation);

  /**
   * @brief このEntityのスケールを取得する
   * @return このEntityのスケール
   */
  const glm::vec3 GetScale() const;

  /**
   * @brief このEntityのスケールを設定する
   * @param scale 新しいスケール
   *
   * 内部で保持するModel行列の再計算が行われます。
   */
  void SetScale(const glm::vec3 scale);

...

  /**
   * @brief Model行列を再計算する
   */
  void RecaluculateModelMatrix();
mesh_entity.cpp
...

const glm::vec3 MeshEntity::GetPosition() const { return position_; }

void MeshEntity::SetPosition(const glm::vec3 position) {
  position_ = position;
  RecaluculateModelMatrix();
}

const glm::vec3 MeshEntity::GetRotation() const { return rotation_; }

void MeshEntity::SetRotation(const glm::vec3 rotation) {
  rotation_ = rotation;
  RecaluculateModelMatrix();
}

const glm::vec3 MeshEntity::GetScale() const { return scale_; }

void MeshEntity::SetScale(const glm::vec3 scale) {
  scale_ = scale;
  RecaluculateModelMatrix();
}

...

void MeshEntity::RecaluculateModelMatrix() {
  auto scale = glm::scale(glm::mat4(1), scale_);
  auto rotation = glm::eulerAngleYXZ(rotation_.y, rotation_.x, rotation_.z);
  auto translate = glm::translate(glm::mat4(1), position_);
  model_matrix_ = translate * rotation * scale;
}

SetterではModel行列の再計算を行っています。 Model行列は拡大縮小、回転、平行移動の順に掛け合わせています。

Model行列を取得するメンバ関数も用意しています。

mesh_entity.h
  /**
   * @brief Model行列を取得する
   * @return Model行列
   */
  const glm::mat4 GetModelMatrix() const;
mesh_entity.cpp
const glm::mat4 MeshEntity::GetModelMatrix() const { return model_matrix_; }

コンストラクタではMeshと位置、回転、拡大縮小を渡しています。

mesh_entity.h
  /**
   * @brief コンストラクタ
   * @param mesh Meshのshared_ptr
   * @param material Materialのshared_ptr
   * @param position Entityの位置
   * @param rotation Entityの回転のオイラー角
   * @param scale Entityの各軸のスケール
   *
   * オイラー角はYXZの順です。
   */
  MeshEntity(const std::shared_ptr<const Mesh> mesh, const glm::vec3 position,
             const glm::vec3 rotation, const glm::vec3 scale);
mesh_entity.cpp
MeshEntity::MeshEntity(const std::shared_ptr<const Mesh> mesh,
                       const glm::vec3 position, const glm::vec3 rotation,
                       const glm::vec3 scale)
    : mesh_(mesh),
      position_(position),
      rotation_(rotation),
      scale_(scale),
      model_matrix_() {
  RecaluculateModelMatrix();
}

Cameraクラスの作成

カメラの行列を保持するクラスを作成します。

camera.h
#ifndef OPENGL_PBR_MAP_CAMERA_H_
#define OPENGL_PBR_MAP_CAMERA_H_

#include <GL/glew.h>
#include <GLFW/glfw3.h>

#include <glm/glm.hpp>
#include <glm/gtx/euler_angles.hpp>

namespace game {

/**
 * @brief カメラを表現するクラス
 */
class Camera {
 public:
  /**
   * @brief カメラの位置を取得する
   * @return カメラの位置
   */
  const glm::vec3 GetPosition() const;

  /**
   * @brief カメラの位置を設定する
   * @param position 新しい位置
   *
   * 内部で保持するView行列の再計算が行われます。
   */
  void SetPosition(const glm::vec3 position);

  /**
   * @brief カメラの回転のオイラー角を取得する
   * @return カメラの回転のオイラー角
   *
   * オイラー角はYXZの順です。
   */
  const glm::vec3 GetRotation() const;

  /**
   * @brief カメラの回転のオイラー角を設定する
   * @param rotation 新しい回転のオイラー角
   *
   * オイラー角はYXZの順です。
   * 内部で保持するView行列の再計算が行われます。
   */
  void SetRotation(const glm::vec3 rotation);

  /**
   * @brief カメラのProjection行列のパラメータを設定する
   * @param fovy fovyの値(radians)
   * @param aspect aspectの値
   * @param near nearの値
   * @param far farの値
   */
  void Perspective(const GLfloat fovy, const GLfloat aspect, const GLfloat near,
                   const GLfloat far);

  /**
   * @brief ViewProjection行列を計算し取得する
   * @return 計算されたViewProjection行列
   *
   * 呼び出すたびにview_matrix_とprojection_matrix_の掛け算が行われます。
   */
  const glm::mat4 GetViewProjectionMatrix() const;

  /**
   * @brief View行列を取得する
   * @return View行列
   */
  const glm::mat4 GetViewMatrix() const;

  /**
   * @brief Projection行列を取得する
   * @return Projection行列
   */
  const glm::mat4 GetProjectionMatrix() const;

  /**
   * @brief コンストラクタ
   * @param position カメラの位置
   * @param rotation カメラの回転のオイラー角
   * @param fovy fovyの値(radians)
   * @param aspect aspectの値
   * @param near nearの値
   * @param far farの値
   *
   * 必要なパラメータをすべて受け取るコンストラクタです。
   * オイラー角はYXZの順です。
   */
  Camera(const glm::vec3 position, const glm::vec3 rotation, const GLfloat fovy,
         const GLfloat aspect, const GLfloat near, const GLfloat far);

 private:
  glm::vec3 position_;
  glm::vec3 rotation_;
  glm::mat4 view_matrix_;
  glm::mat4 projection_matrix_;

  /**
   * @brief View行列を再計算する
   *
   * view_matrix_の値をposition_とrotation_から計算した値に設定します。
   */
  void RecaluculateViewMatirx();
};

}  // namespace game

#endif  // OPENGL_PBR_MAP_CAMERA_H_
camera.cpp
#include "camera.h"

namespace game {

const glm::vec3 Camera::GetPosition() const { return position_; }

void Camera::SetPosition(const glm::vec3 position) {
  position_ = position;
  RecaluculateViewMatirx();
}

const glm::vec3 Camera::GetRotation() const { return rotation_; }

void Camera::SetRotation(const glm::vec3 rotation) {
  rotation_ = rotation;
  RecaluculateViewMatirx();
}

void Camera::Perspective(const GLfloat fovy, const GLfloat aspect,
                         const GLfloat near, const GLfloat far) {
  projection_matrix_ = glm::perspective(fovy, aspect, near, far);
}

const glm::mat4 Camera::GetViewProjectionMatrix() const {
  return projection_matrix_ * view_matrix_;
}

const glm::mat4 Camera::GetViewMatrix() const { return view_matrix_; }

const glm::mat4 Camera::GetProjectionMatrix() const {
  return projection_matrix_;
}

Camera::Camera(const glm::vec3 position, const glm::vec3 rotation,
               const GLfloat fovy, const GLfloat aspect, const GLfloat near,
               const GLfloat far)
    : position_(position),
      rotation_(rotation),
      view_matrix_(),
      projection_matrix_(glm::perspective(fovy, aspect, near, far)) {
  RecaluculateViewMatirx();
}

void Camera::RecaluculateViewMatirx() {
  auto rotation = glm::eulerAngleYXZ(rotation_.y, rotation_.x, rotation_.z);
  auto translate = glm::translate(glm::mat4(1), position_);
  view_matrix_ = glm::inverse(translate * rotation);
}

}  // namespace game

View行列とProjection行列を保持しています。 それぞれCamera::SetPositionSetRotationCamera::Perspectiveでパラメータを設定できます。 各行列を取得するGetterも用意しています。

シェーダの変更

頂点シェーダを次のように変更します。 uniform変数で行列を受け取り座標変換を行っています。

shader.vert
#version 460

layout (location = 0) in vec4 position;

uniform mat4 Model;
uniform mat4 ViewProjection;

void main() {
  gl_Position = ViewProjection * Model * position;
}

座標変換については過去に記事を書いたことがあります。

フラグメントシェーダを次のように変更します。 今回は赤色一色で塗りつぶしています。

shader.frag
#version 460

layout (location = 0) out vec4 fragment;

void main() {
  fragment = vec4(1.0, 0.0, 0.0, 1.0);
}

Applicationクラスの変更

インクルードするヘッダを変更します。

#include <GL/glew.h>
#include <GLFW/glfw3.h>

#include <fstream>
#include <glm/glm.hpp>
#include <iostream>
#include <string>

#include "mesh.h"
+#include "mesh_entity.h"
+#include "camera.h"

Applicationクラスに次のメンバ変数をもたせます。

 private:
  GLFWwindow* window_;
  GLuint program_;
+  GLuint model_loc_;
+  GLuint view_projection_loc_;
+  std::vector<MeshEntity> mesh_entities_;
+  std::unique_ptr<Camera> camera_;
-  std::unique_ptr<Mesh> triangle_;

Application::Init内を次のように変更します。

bool Application::Init() {
  const GLuint width = 960;
  const GLuint height = 540;

  if (!InitWindow(width, height)) {
    std::cerr << "Error: InitWindow" << std::endl;
    return false;
  }

  // Shaderプログラムの作成
  program_ = createProgram("shader.vert", "shader.frag");

-  // 三角形メッシュの作成
-  triangle_ = Mesh::CreateTriangleMesh();

+  // Uniform変数の位置を取得
+  model_loc_ = glGetUniformLocation(program_, "Model");
+  view_projection_loc_ = glGetUniformLocation(program_, "ViewProjection");

+  // Meshの読み込み
+  auto mesh = Mesh::LoadObjMesh("monkey.obj");

+  // MeshEntityの作成
+  mesh_entities_.emplace_back(mesh, glm::vec3(0.0f, 0.0f, 0.0f),
+                              glm::vec3(0.0f), glm::vec3(1.0f));
+  mesh_entities_.emplace_back(mesh, glm::vec3(2.0f, 0.0f, 0.0f),
+                              glm::vec3(0.0f), glm::vec3(1.0f));
+  mesh_entities_.emplace_back(mesh, glm::vec3(-2.0f, 0.0f, 0.0f),
+                              glm::vec3(0.0f), glm::vec3(1.0f));

+  // Cameraの作成
+  camera_ = std::make_unique<Camera>(
+      glm::vec3(0.0f, 0.0f, 10.0f), glm::vec3(0.0f), glm::radians(60.0f),
+      static_cast<float>(width) / height, 0.1f, 100.0f);

  return true;
}

glGetUniformLocationで各種Uniform変数の位置を取得しています。 その後、objからMeshを読み込み、Meshを共有するMeshEntityを3つ作成しています。

今回読み込んでいるmonkey.objはBlenderで作成した次のようなメッシュです。

screenshot 1

Application::Update内を次のように変更します。

void Application::Update(const double delta_time) {
  glClear(GL_COLOR_BUFFER_BIT);

  glUseProgram(program_);

-  triangle_->Draw();

+  auto view_projection = camera_->GetViewProjectionMatrix();
+  glUniformMatrix4fv(view_projection_loc_, 1, GL_FALSE, &view_projection[0][0]);
+
+  for (auto&& mesh_entity : mesh_entities_) {
+    auto model = mesh_entity.GetModelMatrix();
+    glUniformMatrix4fv(model_loc_, 1, GL_FALSE, &model[0][0]);
+    mesh_entity.mesh_->Draw();
+  }
}

ついでにデプステストも有効にしておきます。 Application::InitWindowの内部で次のように追記します。

         const auto* message, const void* userParam) {
        auto t = type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "";
        std::cerr << "GL CALLBACK: " << t << " type = " << type
                  << ", severity = " << severity << ", message = " << message
                  << std::endl;
      },
      0);

+  glEnable(GL_DEPTH_TEST);

  return true;
}

Application::Updateを次のように変更します。

void Application::Update(const double delta_time) {
-  glClear(GL_COLOR_BUFFER_BIT);
+  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glUseProgram(program_);

  auto view_projection = camera_->GetViewProjectionMatrix();
  glUniformMatrix4fv(view_projection_loc_, 1, GL_FALSE, &view_projection[0][0]);

  for (auto&& mesh_entity : mesh_entities_) {
    auto model = mesh_entity.GetModelMatrix();
    glUniformMatrix4fv(model_loc_, 1, GL_FALSE, &model[0][0]);
    mesh_entity.mesh_->Draw();
  }
}

実行結果

実行結果は次のようになります。

screenshot 0

真っ赤に塗りつぶされた猿が3つ横に並んでいます。

プログラム全文

プログラム全文はGitHubにアップロードしてあります。

GitHub: MatchaChoco010/OpenGL-PBR-Map at v3