OpenGLでobjをlambertシェーダで表示してみる

はじめに

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

この記事ではobjファイルを読み込みlambertシェーダで表示するところまでを行います。

screenshot 0

プログラムの変更点

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

Meshクラスの変更点

コンストラクタで法線も受け取るようにします。

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

法線のVBO用のメンバ変数を用意します。

mesh.h
 private:
  GLuint size_;
  GLuint vertices_vbo_;
+  GLuint normals_vbo_;
  GLuint vao_;

法線のVBOを作成するようにコンストラクタの実装を変更します。

mesh.cpp
Mesh::Mesh(const std::vector<glm::vec3>& vertices,
           const std::vector<glm::vec3>& normals)
    : 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, &normals_vbo_);
  glBindBuffer(GL_ARRAY_BUFFER, normals_vbo_);
  glBufferData(GL_ARRAY_BUFFER, normals.size() * sizeof(glm::vec3),
               &normals[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);
}

ムーブコンストラクタとムーブ代入演算子にnormals_vbo_を加えます。

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

  other.vertices_vbo_ = 0;
  other.normals_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_;
    normals_vbo_ = other.normals_vbo_;
    other.vertices_vbo_ = 0;
    other.normals_vbo_ = 0;
  }

  return *this;
}

Mesh::Releaseを次のように書き換えます。

mesh.cpp
void Mesh::Release() {
  glDeleteVertexArrays(1, &vao_);
  glDeleteBuffers(1, &vertices_vbo_);
  glDeleteBuffers(1, &normals_vbo_);
}

Mesh::LoadObjMeshを次のように法線情報も読み込むように変更します。

mesh.cpp
std::shared_ptr<Mesh> Mesh::LoadObjMesh(const std::string file) {
  std::vector<unsigned int> vertex_indices, normal_indices;
  std::vector<glm::vec3> tmp_vertices;
  std::vector<glm::vec3> tmp_normals;
  std::vector<glm::vec3> vertices;
  std::vector<glm::vec3> normals;

  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] == "vn") {
      tmp_normals.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]));
      normal_indices.emplace_back(std::stoi(v1[2]));
      normal_indices.emplace_back(std::stoi(v2[2]));
      normal_indices.emplace_back(std::stoi(v3[2]));
    }
  }

  for (unsigned int i = 0; i < vertex_indices.size(); i++) {
    unsigned int vertex_index = vertex_indices[i];
    vertices.emplace_back(tmp_vertices[vertex_index - 1]);
  }
  for (unsigned int i = 0; i < normal_indices.size(); i++) {
    unsigned int normal_index = normal_indices[i];
    normals.emplace_back(tmp_normals[normal_index - 1]);
  }

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

シェーダの変更点

頂点シェーダを次のように変更します。

shader.vert
#version 460

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

out vec3 vWorldPosition;
out vec3 vWorldNormal;

uniform mat4 Model;
uniform mat4 ModelIT;
uniform mat4 ViewProjection;

void main() {
  vec4 worldPosition = Model * position;
  gl_Position = ViewProjection * worldPosition;
  vWorldPosition = worldPosition.xyz / worldPosition.w;
  vWorldNormal = mat3(ModelIT) * normal;
}

ワールド空間法線を求めるのにModel行列の逆転置をかけているのについてはこちらを参考にしてください。

法線の変換の話 - 穴日記

フラグメントシェーダを次のように書き換えます。

shader.frag
#version 460

in vec3 vWorldPosition;
in vec3 vWorldNormal;

layout (location = 0) out vec4 fragment;

const vec3 worldLightPosition = vec3(0.0, 5.0, 2.0);
const vec3 lightColor = vec3(1.0);
const vec3 Kdiff = vec3(0.8);

void main() {
  vec3 L = normalize(worldLightPosition - vWorldPosition);
  vec3 N = normalize(vWorldNormal);

  vec3 color = max(dot(L, N), 0) * Kdiff * lightColor;

  fragment = vec4(color, 1.0);
}

点光源のワールド位置をシェーダコードにベタ書きしています。 Kdiffは拡散反射の反射係数です。

光源の向きLと法線Nの内積をとって、0とmaxをしてランバート反射を計算しています。

今回は光の減衰は考えていません。

Applicationクラスの変更点

インクルードするヘッダを追加します。

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

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

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

ModelIT用のメンバ変数を用意します。

application.h
 private:
  GLFWwindow* window_;
  GLuint program_;
  GLuint model_loc_;
+  GLuint model_it_loc_;
  GLuint view_projection_loc_;
  std::vector<MeshEntity> mesh_entities_;
  std::unique_ptr<Camera> camera_;

Application::InitでUniform変数ModelITのlocationを取得します。

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

Application::UpdateModelITを計算してUniform変数として渡しています。

application.cpp
void Application::Update(const double delta_time) {
  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();
+    auto model_it = glm::inverseTranspose(model);
    glUniformMatrix4fv(model_loc_, 1, GL_FALSE, &model[0][0]);
+    glUniformMatrix4fv(model_it_loc_, 1, GL_FALSE, &model_it[0][0]);
    mesh_entity.mesh_->Draw();
  }
}

objデータ

今回読み込むobjデータは次のような猿です。 面をフラットではなくスムーズにしました。

screenshot 1

実行結果

実行結果は次のとおりです。

screenshot 0

プログラム全文

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

GitHub: MatchaChoco010/OpenGL-PBR-Map at v4