OpenGLでobjにテクスチャを貼り付けて表示してみる

はじめに

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

この記事ではobjにテクスチャを貼り付けて表示するところまでを行います。

screenshot 0

stbの追加

画像の読み込みにstb_image.hを利用します。

次のページからstbをダウンロードします。

nothings/stb: stb single-file public domain libraries for C/C++

screenshot 1

ダウンロードしたzipを解凍して次のように配置します。

root/
├── OpenGL-PBR-Map/
└── vendors/
    ├── glfw/
    ├── glew/
    ├── glm/
    └── stb/

プロジェクトのプロパティからインクルードディレクトリに$(SolutionDir)vendors\stb\を追加します。

screenshot 2

テクスチャの用意

次のテクスチャをmonkey_albedo.pngとして用意しました。

monkey_albedo.png

前回からのプログラムの変更点

前回のプログラムからの変更点を次に示します。

Textureクラス

新しくTextureクラスを作成します。

texture.h
#ifndef OPENGL_PBR_MAP_TEXTURE_H_
#define OPENGL_PBR_MAP_TEXTURE_H_

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

#include <iostream>
#include <string>

namespace game {

/**
 * @brief Textureを表現するオブジェクト
 *
 * OpenGLのTextureを生成しIDを管理します。
 * テクスチャの多重開放を避けるためコピー禁止です。
 * ムーブは可能です。
 * テクスチャの読み込みにはstbのstb_image.hを利用しています。
 * https://github.com/nothings/stb
 */
class Texture {
 public:
  /**
   * @brief テクスチャの幅を取得
   * @return テクスチャの幅
   */
  const int GetWidth() const;

  /**
   * @brief テクスチャの高さを取得
   * @return テクスチャの高さ
   */
  const int GetHeight() const;

  /**
   * @brief テクスチャのチャンネル数を取得
   * @return テクスチャのチャンネル数
   */
  const int GetChannel() const;

  /**
   * @brief テクスチャIDを取得
   * @return テクスチャID
   */
  const GLuint GetTextureId() const;

  /**
   * @brief デフォルトコンストラクタ
   *
   * 何もファイルを読み込まない空のテクスチャとなります。
   * texture_id_は0で初期化されます。
   */
  Texture();

  /**
   * @brief コンストラクタ
   * @param path テクスチャのパス
   * @param sRGB sRGBならばtrue、Linearならばfalse
   */
  Texture(const std::string& path, const bool sRGB);

  /**
   * @brief デストラクタ
   *
   * texture_id_のテクスチャを開放します。
   */
  ~Texture();

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

  /**
   * @brief ムーブコンストラクタ
   * @param other ムーブ元
   *
   * ムーブ後のtexture_id_は0に初期化されます。
   */
  Texture(Texture&& other) noexcept;

  /**
   * @brief ムーブ代入演算子
   * @param other ムーブ元
   *
   * ムーブ後のtexture_id_は0に初期化されます。
   */
  Texture& operator=(Texture&& other) noexcept;

 private:
  int width_;
  int height_;
  int channel_;
  GLuint texture_id_;
};

}  // namespace game

#endif  // OPENGL_PBR_MAP_TEXTURE_H_
texture.cpp
#include "texture.h"

#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>

namespace game {

const int Texture::GetWidth() const { return width_; }

const int Texture::GetHeight() const { return height_; }

const int Texture::GetChannel() const { return channel_; }

const GLuint Texture::GetTextureId() const { return texture_id_; }

Texture::Texture() : width_(0), height_(0), channel_(0), texture_id_(0) {}

Texture::Texture(const std::string& path, const bool sRGB) {
  stbi_set_flip_vertically_on_load(true);
  unsigned char* data =
      stbi_load(path.c_str(), &width_, &height_, &channel_, 0);

  if (!data) {
    std::cerr << "Can't load image: " << path << std::endl;
    texture_id_ = 0;
  } else {
    glGenTextures(1, &texture_id_);
    glBindTexture(GL_TEXTURE_2D, texture_id_);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                    GL_LINEAR_MIPMAP_LINEAR);

    if (sRGB) {
      if (channel_ == 4) {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, width_, height_, 0,
                     GL_RGBA, GL_UNSIGNED_BYTE, data);
      } else if (channel_ == 3) {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8, width_, height_, 0, GL_RGB,
                     GL_UNSIGNED_BYTE, data);
      }
    } else {
      if (channel_ == 4) {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width_, height_, 0, GL_RGBA,
                     GL_UNSIGNED_BYTE, data);
      } else if (channel_ == 3) {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width_, height_, 0, GL_RGB,
                     GL_UNSIGNED_BYTE, data);
      } else if (channel_ == 1) {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, width_, height_, 0, GL_RED,
                     GL_UNSIGNED_BYTE, data);
      }
    }

    glGenerateMipmap(GL_TEXTURE_2D);
    stbi_image_free(data);

    glBindTexture(GL_TEXTURE_2D, 0);
  }
}

Texture::~Texture() { glDeleteTextures(1, &texture_id_); }

Texture::Texture(Texture&& other) noexcept
    : texture_id_(other.texture_id_),
      width_(other.width_),
      height_(other.height_),
      channel_(other.channel_) {
  other.texture_id_ = 0;
}

Texture& Texture::operator=(Texture&& other) noexcept {
  if (this != &other) {
    glDeleteTextures(1, &texture_id_);
    texture_id_ = other.texture_id_;
    other.texture_id_ = 0;

    width_ = other.width_;
    height_ = other.height_;
    channel_ = other.channel_;
  }

  return *this;
}

}  // namespace game

例によってGLのリソースなので多重開放を防ぐためにコピー禁止にしています。

sRGBなテクスチャかLinearなテクスチャかを受け取って分岐していることに注意です。 今回のテクスチャはsRGBですが、今後Linearなテクスチャも使います。

Materialクラス

新たにMaterialクラスを作成します。

material.h
#ifndef OPENGL_PBR_MAP_MATERIAL_H_
#define OPENGL_PBR_MAP_MATERIAL_H_

#include "texture.h"

namespace game {

/**
 * @brief マテリアルを表現するオブジェクト
 *
 * PBRマテリアルに必要な各種テクスチャとEmissiveの強さを保持します。
 */
class Material {
 public:
  const Texture albedo_map_;

  /**
   * @brief コンストラクタ
   * @param albedo_map アルベドテクスチャ
   *
   * 各種テクスチャを指定してインスタンスを作成します。
   */
  Material(Texture&& albedo_map);
};

}  // namespace game

#endif  // OPENGL_PBR_MAP_MATERIAL_H_
material.cpp
#include "material.h"

namespace game {

Material::Material(Texture&& albedo_map)
    : albedo_map_(std::move(albedo_map)) {}

}  // namespace game

albedo_mapのテクスチャを保持します。 今後テクスチャが増えていきますが、ひとまず一枚だけ。

MeshEntityクラスの変更点

MeshEntityクラスにマテリアルをもたせるように変更します。

mesh_entity.h
+#include "material.h"
#include "mesh.h"
mesh_entity.h
 */
class MeshEntity {
 public:
  const std::shared_ptr<const Mesh> mesh_;
+  const std::shared_ptr<const Material> material_;
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 std::shared_ptr<const Material> material,
             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 std::shared_ptr<const Material> material,
                       const glm::vec3 position, const glm::vec3 rotation,
                       const glm::vec3 scale)
    : mesh_(mesh),
+      material_(material),
      position_(position),
      rotation_(rotation),
      scale_(scale),
      model_matrix_() {
  RecaluculateModelMatrix();
}

Meshクラスの変更点

UV座標を取得するようにコンストラクタを変更します。

mesh.h
  /**
   * @brief コンストラクタ
   * @param vertices 頂点位置の列
   * @param normals 頂点法線の列
+   * @param uvs UV座標の列
   *
   * ジオメトリを構築し、VBOとVAOを構築します。
   * 各種頂点情報は前から順に3つずつで一つの面を構成していきます。
   */
  Mesh(const std::vector<glm::vec3>& vertices,
-       const std::vector<glm::vec3>& normals);
+       const std::vector<glm::vec3>& normals,
+       const std::vector<glm::vec2>& uvs);
mesh.h
 private:
  GLuint size_;
  GLuint vertices_vbo_;
  GLuint normals_vbo_;
+  GLuint uvs_vbo_;
  GLuint vao_;
mesh.cpp
Mesh::Mesh(const std::vector<glm::vec3>& vertices,
-           const std::vector<glm::vec3>& normals)
+           const std::vector<glm::vec3>& normals,
+           const std::vector<glm::vec2>& uvs)
    : 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));
+
+  glGenBuffers(1, &uvs_vbo_);
+  glBindBuffer(GL_ARRAY_BUFFER, uvs_vbo_);
+  glBufferData(GL_ARRAY_BUFFER, uvs.size() * sizeof(glm::vec2), &uvs[0],
+               GL_STATIC_DRAW);
+  glEnableVertexAttribArray(2);
+  glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, static_cast<void*>(0));

  glBindBuffer(GL_ARRAY_BUFFER, 0);
  glBindVertexArray(0);
}

Mesh::LoadObjMeshをUV座標を読み込むように変更します。

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

  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] == "vt") {
+      tmp_uvs.emplace_back(std::stof(col[1]), std::stof(col[2]));
    } 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]));
+      uv_indices.emplace_back(std::stoi(v1[1]));
+      uv_indices.emplace_back(std::stoi(v2[1]));
+      uv_indices.emplace_back(std::stoi(v3[1]));
    }
  }

  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]);
  }
+  for (unsigned int i = 0; i < uv_indices.size(); i++) {
+    unsigned int uv_index = uv_indices[i];
+    uvs.emplace_back(tmp_uvs[uv_index - 1]);
+  }

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

シェーダの変更点

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

shader.vert
#version 460

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

out vec3 vWorldPosition;
out vec3 vWorldNormal;
out vec2 vUv;

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;
  vUv = uv;
}

UV座標を頂点属性として読み込んでvUvでフラグメントシェーダに受け渡しています。

フラグメントシェーダを次のようにします。

shader.frag
#version 460

in vec3 vWorldPosition;
in vec3 vWorldNormal;
in vec2 vUv;

layout (location = 0) out vec4 fragment;

uniform vec3 worldCameraPosition;

layout (binding = 0) uniform sampler2D albedoMap;

const vec3 worldLightPosition = vec3(0.0, 5.0, 2.0);

const vec3 lightColor = vec3(1.0);

const vec3 Kspec = vec3(1.0);
const float shininess = 50;

void main() {
  vec3 Kdiff = texture(albedoMap, vUv).rgb;

  vec3 L = normalize(worldLightPosition - vWorldPosition);
  vec3 V = normalize(worldCameraPosition - vWorldPosition);
  vec3 N = normalize(vWorldNormal);
  vec3 H = normalize(L + V);

  // Lambert
  vec3 diffuse = max(dot(L, N), 0) * Kdiff * lightColor;

  // Blinn-phong
  vec3 specular = pow(max(dot(N, H), 0), shininess) * Kspec * lightColor;

  vec3 color = diffuse + specular;

  fragment = vec4(color, 1.0);
}

前回からの変更点はKdiffを定数から読み込んだテクスチャのrgb値を利用するようにした点です。

Applicationクラスの変更点

Application::Init内でマテリアルを作成します。

application.cpp
  // Materialの作成
  auto material =
      std::make_shared<Material>(Texture("monkey_albedo.png", true));

MeshEntityの作成時にマテリアルを追加します。

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

Application::Update内で次のようにしてテクスチャをバインドします。

application.cpp
  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]);

    glActiveTexture(GL_TEXTURE0);    glBindTexture(GL_TEXTURE_2D,                  mesh_entity.material_->albedo_map_.GetTextureId());
    mesh_entity.mesh_->Draw();
  }

実行結果

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

screenshot 0

プログラム全文

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

GitHub: MatchaChoco010/OpenGL-PBR-Map at v6