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

はじめに

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

この記事ではOpenGLで三角形を描画するところまでを行います。

screenshot 0

プログラムの変更点

前回の記事からの変更点を次に示します。

includeするファイルの追加

まずはincludeするファイルの追加です。

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

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

後に使うヘッダファイルをインクルードしました。

main関数の変更点

main関数の内部に次のように追記します。

main.cpp
  ...

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

  // Shaderプログラムの作成  const GLuint program = createProgram("shader.vert", "shader.frag");  // 表示する三角形の作成  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),  };  // VAOの作成とバインド  GLuint vao;  glGenVertexArrays(1, &vao);  glBindVertexArray(vao);  // VBOを作成  GLuint verticesVbo;  glGenBuffers(1, &verticesVbo);  glBindBuffer(GL_ARRAY_BUFFER, verticesVbo);  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));  GLuint colorsVbo;  glGenBuffers(1, &colorsVbo);  glBindBuffer(GL_ARRAY_BUFFER, colorsVbo);  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));
  // メインループ
  while (glfwWindowShouldClose(window) == GL_FALSE) {
    glClear(GL_COLOR_BUFFER_BIT);    glUseProgram(program);    glBindVertexArray(vao);    glDrawArrays(GL_TRIANGLES, 0, vertices.size());
    glfwSwapBuffers(window);
    glfwPollEvents();
  }
  ...

次の部分でシェーダをコンパイルし、programを取得しています。 createProgramは後ほど説明しますが、引数はシェーダファイルのパスです。 シェーダファイルについても後ほど説明します。

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

次に三角形のメッシュを作成しています。

次の部分でメッシュを構築する頂点属性をvectorに格納しています。

  // 表示する三角形の作成
  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),
  };

次の部分でVAOを作成しバインドしています。 VAOを利用するとVBOを個別にバインドする手間が省けます。

  // VAOの作成とバインド
  GLuint vao;
  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);

次の部分で頂点位置のVBOを作成しています。 先のVAOをバインドした状態でVBOを作成しているので、このVBOは先に作成したVAOに属します。

  // VBOを作成
  GLuint verticesVbo;
  glGenBuffers(1, &verticesVbo);
  glBindBuffer(GL_ARRAY_BUFFER, verticesVbo);
  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でVBOを作成し、glBindBufferでバインドした後にglBufferDataでデータを流し込んでいます。 この頂点属性はシェーダで0番目として受け取りたいのでglEnableVertexAttribArrayおよびglVertexAttribPointerの第一引数には0を指定しています。

次の部分で頂点カラーを転送しています。

  GLuint colorsVbo;
  glGenBuffers(1, &colorsVbo);
  glBindBuffer(GL_ARRAY_BUFFER, colorsVbo);
  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));

頂点カラー用のVBOを作成し、1番のattributeとして渡しています。

メインループではglClearでカラーバッファをクリアした後、glUseProgramでシェーダプログラムを有効にして、glBindVertexArrayでVAOをバインドし、glDrawArraysで三角形を描画しています。

  // メインループ
  while (glfwWindowShouldClose(window) == GL_FALSE) {
    glClear(GL_COLOR_BUFFER_BIT);

    glUseProgram(program);

    glBindVertexArray(vao);
    glDrawArrays(GL_TRIANGLES, 0, vertices.size());

    glfwSwapBuffers(window);
    glfwPollEvents();
  }

createProgram関数

main関数の他にcreateProgram関数を作成しました。 createProgramは次のとおりです。

main.cpp
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;
}

最初に頂点シェーダのファイルの中身をテキストとして読み込んでいます。 C形式の文字列が必要なため、c_str()で取得しています。

  // 頂点シェーダの読み込み
  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();

次にprogramオブジェクトを作成しています。

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

次のようにして頂点シェーダをコンパイルし、プログラムオブジェクトにアタッチします。

  // 頂点シェーダのコンパイル
  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;
  }

最後に作成したprogramのIDを返しています。

  return program;

頂点シェーダ

プロジェクトにshader.vertというファイルを作成します。 作成したファイルに次のように記述します。

#version 460

layout (location = 0) in vec4 position;
layout (location = 1) in vec3 vertexColor;

out vec3 color;

void main() {
  color = vertexColor;
  gl_Position = position;
}

最初の#version 460はGLSLのバージョン情報です。

次の部分では頂点属性(attribute)を記述しています。

layout (location = 0) in vec4 position;
layout (location = 1) in vec3 vertexColor;

layout (location = 0)で0番目の頂点属性を受け取り、layout (location = 1)で1番目の頂点属性を受け取ります。 main.cppのmain関数内でglVertexAttribPointerを利用して0番目に頂点位置を、1番目に頂点カラーを割り当てました。 その頂点属性を受け取っています。

out vec3 color;でフラグメントシェーダに受け渡す変数を定義しています。 フラグメントシェーダには色情報を受け渡します。

shader.vertのmain関数では先に定義したフラグメントシェーダに受け渡すcolorに頂点カラーを、組み込みのgl_Positionに頂点位置を代入しています。

void main() {
  color = vertexColor;
  gl_Position = position;
}

フラグメントシェーダ

プロジェクトにshader.fragというファイルを作成します。 作成したファイルに次のように記述します。

#version 460

in vec3 color;

layout (location = 0) out vec4 fragment;

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

最初の#version 460はGLSLのバージョン情報です。

次の部分で頂点シェーダから渡される色情報を受け取る変数を定義しています。

in vec3 color;

次の部分でフレームバッファに書き込む色情報の変数を定義しています。

layout (location = 0) out vec4 fragment;

locationを指定しています。 今回はデフォルトフレームバッファに書き込んでいるのでそれを指定していることになります。

main関数では出力のfragmentに頂点シェーダから渡されてきた色を渡しています。

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

実行結果

プログラムを実行すると次のようになります。

screenshot 0

頂点カラーがフラグメントシェーダに渡される過程で補間されるので、グラデーションになります。

プログラム全文

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

GitHub: MatchaChoco010/OpenGL-PBR-Map at v1

参考資料

今回は細かい解説はだいぶ省いてしまいました。 細かい部分について知りたい場合は次の資料がとても役に立ちます。

GLFW による OpenGL 入門