OpenGLでobjにテクスチャを貼り付けて表示してみる
はじめに
この記事はシリーズ記事です。目次はこちら。
この記事ではobjにテクスチャを貼り付けて表示するところまでを行います。
stbの追加
画像の読み込みにstb_image.hを利用します。
次のページからstbをダウンロードします。
nothings/stb: stb single-file public domain libraries for C/C++
ダウンロードしたzipを解凍して次のように配置します。
root/
├── OpenGL-PBR-Map/
└── vendors/
├── glfw/
├── glew/
├── glm/
└── stb/
プロジェクトのプロパティからインクルードディレクトリに$(SolutionDir)vendors\stb\
を追加します。
テクスチャの用意
次のテクスチャをmonkey_albedo.pngとして用意しました。
前回からのプログラムの変更点
前回のプログラムからの変更点を次に示します。
Textureクラス
新しくTextureクラスを作成します。
#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_
#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クラスを作成します。
#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_
#include "material.h"
namespace game {
Material::Material(Texture&& albedo_map)
: albedo_map_(std::move(albedo_map)) {}
} // namespace game
albedo_map
のテクスチャを保持します。
今後テクスチャが増えていきますが、ひとまず一枚だけ。
MeshEntityクラスの変更点
MeshEntityクラスにマテリアルをもたせるように変更します。
+#include "material.h"
#include "mesh.h"
*/
class MeshEntity {
public:
const std::shared_ptr<const Mesh> mesh_;
+ const std::shared_ptr<const Material> material_;
/**
* @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);
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座標を取得するようにコンストラクタを変更します。
/**
* @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);
private:
GLuint size_;
GLuint vertices_vbo_;
GLuint normals_vbo_;
+ GLuint uvs_vbo_;
GLuint vao_;
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座標を読み込むように変更します。
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);
}
シェーダの変更点
頂点シェーダを次のようにします。
#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
でフラグメントシェーダに受け渡しています。
フラグメントシェーダを次のようにします。
#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
内でマテリアルを作成します。
// Materialの作成
auto material =
std::make_shared<Material>(Texture("monkey_albedo.png", true));
MeshEntityの作成時にマテリアルを追加します。
// 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
内で次のようにしてテクスチャをバインドします。
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();
}
実行結果
実行結果は次のようになります。
プログラム全文
プログラム全文はGitHubにアップロードしてあります。