OpenGLでobjをlambertシェーダで表示してみる
はじめに
この記事はシリーズ記事です。目次はこちら。
この記事ではobjファイルを読み込みlambertシェーダで表示するところまでを行います。
プログラムの変更点
前回からの変更点を次に示します。
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::Update
でModelIT
を計算して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データは次のような猿です。 面をフラットではなくスムーズにしました。
実行結果
実行結果は次のとおりです。
プログラム全文
プログラム全文はGitHubにアップロードしてあります。