OpenGLで三角形を表示してみる その2

はじめに

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

この記事では前回作成したmainにベタ書きのプログラムを、今後の拡張を見据えて整理します。

screenshot 0

main関数

main.cppのmain関数を次のように書き換えます。

main.cpp
#include "application.h"

int main() {
  game::Application app;
  return app.Run() ? 0 : -1;
}

application.hのApplicationクラスについては次に説明します。

Applicationクラス

ウィンドウやメインループなどの管理を行うApplicationクラスを作成します。

前回main関数で実行していた初期化処理がApplication::Init()に、ループ処理がApplication::Update(delta_time)に分離されています。

application.h
#ifndef OPENGL_PBR_MAP_APPLICATION_H_
#define OPENGL_PBR_MAP_APPLICATION_H_

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

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

#include "mesh.h"

namespace game {

class Application {
 public:
  /**
   * @brief アプリケーションのエントリーポイント
   * @return 正常終了ならばtrue、異常終了ならばfalse
   *
   * アプリケーションのエントリーポイントです。
   * 内部でループを回しゲームを実行します。
   * ウィンドウが閉じられた場合、ゲームが終了した場合、
   * 何かしらのエラーで停止した場合などにこのメソッドが終了します。
   * 戻り値は正常終了ならばtrue、異常終了ならばfalseです。
   *
   * ```
   * int main() {
   *     game::Application app;
   *     return app.Run() ? 0 : -1;
   * }
   * ```
   */
  bool Run();

 private:
  GLFWwindow* window_;
  GLuint program_;
  std::unique_ptr<Mesh> triangle_;

  /**
   * @brief 初期化処理
   * @return 正常終了ならばtrue、異常終了ならばfalse
   */
  bool Init();

  /**
   * @brief GLFWのウィンドウを作成する
   * @param width ウィンドウの幅
   * @param height ウィンドウの高さ
   * @return 正常終了ならばtrue、異常終了ならばfalse
   *
   * GLFWの初期化とウィンドウ作成、GLEWの初期化、
   * その他GLの初期設定を行います。
   */
  bool InitWindow(const GLuint width, const GLuint height);

  /**
   * @brief 毎フレーム呼ばれる処理
   * @param delta_time 前フレームとの差分時間(秒)
   */
  void Update(const double delta_time);
};

}  // namespace game

#endif  //  OPENGL_PBR_MAP_APPLICATION_H_

実装は次のとおりです。

application.cpp
#include "application.h"

namespace game {

GLuint createProgram(std::string vertexShaderFile,
                     std::string fragmentShaderFile) {
  // 頂点シェーダの読み込み
  std::ifstream vertexIfs(vertexShaderFile, std::ios::binary);
  if (vertexIfs.fail()) {
    std::cerr << "Error: Can't open source file: " << vertexShaderFile
              << std::endl;
    return 0;
  }
  auto vertexShaderSource =
      std::string(std::istreambuf_iterator<char>(vertexIfs),
                  std::istreambuf_iterator<char>());
  if (vertexIfs.fail()) {
    std::cerr << "Error: Can't read source file: " << vertexShaderFile
              << std::endl;
    return 0;
  }
  const GLchar* vertexShaderSourcePointer = vertexShaderSource.c_str();

  // フラグメントシェーダの読み込み
  std::ifstream fragmentIfs(fragmentShaderFile, std::ios::binary);
  if (fragmentIfs.fail()) {
    std::cerr << "Error: Can't open source file: " << fragmentShaderFile
              << std::endl;
    return 0;
  }
  auto fragmentShaderSource =
      std::string(std::istreambuf_iterator<char>(fragmentIfs),
                  std::istreambuf_iterator<char>());
  if (fragmentIfs.fail()) {
    std::cerr << "Error: Can't read source file: " << fragmentShaderFile
              << std::endl;
    return 0;
  }
  const GLchar* fragmentShaderSourcePointer = fragmentShaderSource.c_str();

  // プログラムオブジェクトを作成
  const GLuint program = glCreateProgram();

  GLint status = GL_FALSE;
  GLsizei infoLogLength;

  // 頂点シェーダのコンパイル
  const GLuint vertexShaderObj = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(vertexShaderObj, 1, &vertexShaderSourcePointer, nullptr);
  glCompileShader(vertexShaderObj);
  glAttachShader(program, vertexShaderObj);

  // 頂点シェーダのチェック
  glGetShaderiv(vertexShaderObj, GL_COMPILE_STATUS, &status);
  if (status == GL_FALSE)
    std::cerr << "Compile Error in Vertex Shader." << std::endl;
  glGetShaderiv(vertexShaderObj, GL_INFO_LOG_LENGTH, &infoLogLength);
  if (infoLogLength > 1) {
    std::vector<GLchar> vertexShaderErrorMessage(infoLogLength);
    glGetShaderInfoLog(vertexShaderObj, infoLogLength, nullptr,
                       vertexShaderErrorMessage.data());
    std::cerr << vertexShaderErrorMessage.data() << std::endl;
  }

  glDeleteShader(vertexShaderObj);

  // フラグメントシェーダのコンパイル
  const GLuint fragmentShaderObj = glCreateShader(GL_FRAGMENT_SHADER);
  glShaderSource(fragmentShaderObj, 1, &fragmentShaderSourcePointer, nullptr);
  glCompileShader(fragmentShaderObj);
  glAttachShader(program, fragmentShaderObj);

  // フラグメントシェーダのチェック
  glGetShaderiv(fragmentShaderObj, GL_COMPILE_STATUS, &status);
  if (status == GL_FALSE)
    std::cerr << "Compile Error in Fragment Shader." << std::endl;
  glGetShaderiv(fragmentShaderObj, GL_INFO_LOG_LENGTH, &infoLogLength);
  if (infoLogLength > 1) {
    std::vector<GLchar> fragmentShaderErrorMessage(infoLogLength);
    glGetShaderInfoLog(fragmentShaderObj, infoLogLength, nullptr,
                       fragmentShaderErrorMessage.data());
    std::cerr << fragmentShaderErrorMessage.data() << std::endl;
  }

  glDeleteShader(fragmentShaderObj);

  // プログラムのリンク
  glLinkProgram(program);

  // リンクのチェック
  glGetProgramiv(program, GL_LINK_STATUS, &status);
  if (status == GL_FALSE) std::cerr << "Link Error." << std::endl;
  glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLength);
  if (infoLogLength > 1) {
    std::vector<GLchar> programLinkErrorMessage(infoLogLength);
    glGetProgramInfoLog(program, infoLogLength, nullptr,
                        programLinkErrorMessage.data());
    std::cerr << programLinkErrorMessage.data() << std::endl;
  }

  return program;
}

bool Application::Run() {
  if (!Init()) {
    std::cerr << "Initialization error..." << std::endl;
    return false;
  }

  glfwSetTime(0.0);
  double delta_time = 0.0;
  double prev_time = 0.0;

  while (glfwWindowShouldClose(window_) == GL_FALSE) {
    const double time = glfwGetTime();
    delta_time = time - prev_time;
    prev_time = time;

    Update(delta_time);

    glfwSwapBuffers(window_);
    glfwPollEvents();
  }

  glfwTerminate();

  return true;
}

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();

  return true;
}

bool Application::InitWindow(const GLuint width, const GLuint height) {
  glfwSetErrorCallback(
      [](auto id, auto description) { std::cerr << description << std::endl; });

  // GLFWの初期化
  if (!glfwInit()) {
    return false;
  }

  // OpenGL Version 4.6 Core Profileを選択する
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
  glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

  // リサイズ不可
  glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);

  // ウィンドウの作成
  window_ = glfwCreateWindow(width, height, "Game", nullptr, nullptr);
  if (window_ == nullptr) {
    std::cerr << "Can't create GLFW window." << std::endl;
    return false;
  }
  glfwMakeContextCurrent(window_);

  // GLEWの初期化
  if (glewInit() != GLEW_OK) {
    std::cerr << "Can't initialize GLEW." << std::endl;
    return false;
  }

  // VSyncを待つ
  glfwSwapInterval(1);

  // OpenGL エラーのコールバック
  glEnable(GL_DEBUG_OUTPUT);
  glDebugMessageCallback(
      [](auto source, auto type, auto id, auto severity, auto length,
         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);

  return true;
}

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

  glUseProgram(program_);

  triangle_->Draw();
}

}  // namespace game

次のApplication::Runがメインの処理の部分です。 最初にApplication::Initを呼び出し、メインループの内部でApplication::Updateを呼び出しています。

bool Application::Run() {
  if (!Init()) {
    std::cerr << "Initialization error..." << std::endl;
    return false;
  }

  glfwSetTime(0.0);
  double delta_time = 0.0;
  double prev_time = 0.0;

  while (glfwWindowShouldClose(window_) == GL_FALSE) {
    const double time = glfwGetTime();
    delta_time = time - prev_time;
    prev_time = time;

    Update(delta_time);

    glfwSwapBuffers(window_);
    glfwPollEvents();
  }

  glfwTerminate();

  return true;
}

Application::Initは次のとおりです。 InitWindowでウィンドウの作成やGLの初期化処理を行っています。 createProgramは前回と同様の関数です。 Mesh::CreateTriangleMeshは後で説明します。

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();

  return true;
}

Application::Updateは次のとおりです。

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

  glUseProgram(program_);

  triangle_->Draw();
}

Meshクラス

MeshのVAOなどを扱うクラスを作成します。

mesh.h
#ifndef OPENGL_PBR_MAP_MESH_H_
#define OPENGL_PBR_MAP_MESH_H_

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

#include <glm/glm.hpp>
#include <memory>
#include <vector>

namespace game {

class Mesh {
 public:
  /**
   * @brief VAOをバインドし描画する
   */
  void Draw() const;

  /**
   * @brief コンストラクタ
   * @param vertices 頂点位置の列
   * @param colors 頂点色の列
   *
   * ジオメトリを構築し、VBOとVAOを構築します。
   * 各種頂点情報は前から順に3つずつで一つの面を構成していきます。
   */
  Mesh(const std::vector<glm::vec3>& vertices,
       const std::vector<glm::vec3>& colors);

  /**
   * @brief デストラクタ
   *
   * VBOとVAOを開放します。
   */
  ~Mesh();

  // コピー禁止
  Mesh(const Mesh&) = delete;
  Mesh& operator=(const Mesh&) = delete;

  /**
   * @brief ムーブコンストラクタ
   * @param other ムーブ元
   *
   * ムーブ後はVAO及びVBOは0に設定されます。
   */
  Mesh(Mesh&& other) noexcept;

  /**
   * @brief ムーブ代入演算子
   * @param other ムーブ元
   *
   * ムーブ後はVAO及びVBOは0に設定されます。
   */
  Mesh& operator=(Mesh&& other) noexcept;

  /**
   * @brief 三角形メッシュを作成する静的メンバ関数
   * @return 三角形メッシュ
   */
  static std::unique_ptr<Mesh> CreateTriangleMesh();

 private:
  GLuint size_;
  GLuint vertices_vbo_;
  GLuint colors_vbo_;
  GLuint vao_;

  /**
   * @brief OpenGLのオブジェクトを開放する
   *
   * コンストラクタで確保したVAOとVBOを開放します。
   */
  void Release();
};

}  // namespace game

#endif  // OPENGL_PBR_MAP_MESH_H_

実装は次のとおりです。

mesh.cpp
#include "mesh.h"

namespace game {

void Mesh::Draw() const {
  glBindVertexArray(vao_);
  glDrawArrays(GL_TRIANGLES, 0, size_);
}

Mesh::Mesh(const std::vector<glm::vec3>& vertices,
           const std::vector<glm::vec3>& colors)
    : 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);
}

Mesh::~Mesh() { Release(); }

Mesh::Mesh(Mesh&& other) noexcept
    : size_(std::move(other.size_)),
      vao_(other.vao_),
      vertices_vbo_(other.vertices_vbo_),
      colors_vbo_(other.colors_vbo_) {
  other.vao_ = 0;

  other.vertices_vbo_ = 0;
  other.colors_vbo_ = 0;
}

Mesh& Mesh::operator=(Mesh&& other) noexcept {
  if (this != &other) {
    Release();

    size_ = std::move(other.size_);

    vao_ = other.vao_;
    other.vao_ = 0;

    vertices_vbo_ = other.vertices_vbo_;
    colors_vbo_ = other.colors_vbo_;
    other.vertices_vbo_ = 0;
    other.colors_vbo_ = 0;
  }

  return *this;
}

std::unique_ptr<Mesh> Mesh::CreateTriangleMesh() {
  // 頂点位置
  const std::vector<glm::vec3> vertices = {
      glm::vec3(0.0f, 0.5f, 0.0f),
      glm::vec3(-0.5f, -0.5f, 0.0f),
      glm::vec3(0.5f, -0.5f, 0.0f),
  };
  // 頂点カラー
  const std::vector<glm::vec3> colors = {
      glm::vec3(1.0f, 0.0f, 0.0f),
      glm::vec3(0.0f, 1.0f, 0.0f),
      glm::vec3(0.0f, 0.0f, 1.0f),
  };
  return std::make_unique<Mesh>(vertices, colors);
}

void Mesh::Release() {
  glDeleteVertexArrays(1, &vao_);
  glDeleteBuffers(1, &vertices_vbo_);
  glDeleteBuffers(1, &colors_vbo_);
}

}  // namespace game

GLのオブジェクトを管理するため、コピー禁止でムーブを実装していることに注意です。 コピー禁止をしないとすでに開放したリソースをデストラクタで多重開放してしまいます。 詳しくは次のページを参照してください。

Common Mistakes - OpenGL Wiki #TheObjectOrientedLanguageProblem

OpenGLのオブジェクトを管理するクラスは、このようにコピー禁止にしてムーブを実装するというのが良いようです。

注意としては、コンストラクタでリソースを作成していますが、オブジェクト生成/消失時にOpenGLのコンテキストがカレントになっていなければクラッシュしてしまうということです。

Mesh::CreateTriangleMeshで前回と同様の三角形を作成しています。

実行結果

実行結果は前回とは変わりません。

screenshot 0

プログラム全文

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

GitHub: MatchaChoco010/OpenGL-PBR-Map at v2