OpenGLで法線マップを試してみる
はじめに
この記事はシリーズ記事です。目次はこちら。
この記事では法線マップを使って球を表示するところまでを行います。
モデルと法線マップの用意
法線マップの効果をわかりやすくするために猿ではなく球を使うことにします。
次のテクスチャをnormal.pngとして用意しました。
次のテクスチャをalbedo.pngとして用意しました。
前回からのプログラムの変更点
前回のプログラムからの変更点を次に示します。
Materialクラスの変更点
法線マップをMaterialクラスに追加します。
material.h
/**
* @brief マテリアルを表現するオブジェクト
*
* PBRマテリアルに必要な各種テクスチャとEmissiveの強さを保持します。
*/
class Material {
public:
const Texture albedo_map_;
const Texture normal_map_;
/**
* @brief コンストラクタ
* @param albedo_map アルベドテクスチャ
* @param normal_map ノーマルマップテクスチャ
*
* 各種テクスチャを指定してインスタンスを作成します。
*/
Material(Texture&& albedo_map, Texture&& normal_map);};
material.cpp
Material::Material(Texture&& albedo_map, Texture&& normal_map) : albedo_map_(std::move(albedo_map)), normal_map_(std::move(normal_map)) {}
Meshクラスの変更点
コンストラクタでtangentを計算するようにします。
mesh.h
private:
GLuint size_;
GLuint vertices_vbo_;
GLuint normals_vbo_;
GLuint uvs_vbo_;
GLuint tangents_vbo_; GLuint vao_;
...
/** * @brief Tangentsを計算する * @param vertices 頂点座標 * @param uvs UV * @return 計算されたタンジェント * * verticesとuvsからテクスチャ座標系でのタンジェントを計算し返します。 * vertices、uvs及び返り値のtangentsは、前から順に3頂点で一つの面を構成します。 */ const std::vector<glm::vec3> CalculateTangents( const std::vector<glm::vec3>& vertices, const std::vector<glm::vec2>& uvs);
mesh.cpp
const std::vector<glm::vec3> Mesh::CalculateTangents(
const std::vector<glm::vec3>& vertices, const std::vector<glm::vec2>& uvs) {
std::vector<glm::vec3> tangents;
for (int i = 0; i < vertices.size(); i += 3) {
auto& v0 = vertices[i + 0];
auto& v1 = vertices[i + 1];
auto& v2 = vertices[i + 2];
auto& uv0 = uvs[i + 0];
auto& uv1 = uvs[i + 1];
auto& uv2 = uvs[i + 2];
auto delta_pos1 = v1 - v0;
auto delta_pos2 = v2 - v0;
auto delta_uv1 = uv1 - uv0;
auto delta_uv2 = uv2 - uv0;
float r = 1.0f / (delta_uv1.x * delta_uv2.y - delta_uv1.y * delta_uv2.x);
auto tangent = (delta_pos1 * delta_uv2.y - delta_pos2 * delta_uv1.y) * r;
tangents.emplace_back(tangent);
tangents.emplace_back(tangent);
tangents.emplace_back(tangent);
}
return tangents;
}
mesh.cpp
Mesh::Mesh(const std::vector<glm::vec3>& vertices,
const std::vector<glm::vec3>& normals,
const std::vector<glm::vec2>& uvs)
: size_(vertices.size()) {
auto tangents = CalculateTangents(vertices, uvs);
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));
glGenBuffers(1, &tangents_vbo_); glBindBuffer(GL_ARRAY_BUFFER, tangents_vbo_); glBufferData(GL_ARRAY_BUFFER, tangents.size() * sizeof(glm::vec3), &tangents[0], GL_STATIC_DRAW); glEnableVertexAttribArray(3); glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 0, static_cast<void*>(0));
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
タンジェントの計算については次のページを参考にしました。
UVの三角形が縮退している場合は正しくタンジェントが計算できません。 モデルの作成時に注意が必要です。
シェーダの変更点
頂点シェーダを次のように変更します
layout (location = 0) in vec4 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 uv;
layout (location = 3) in vec3 tangent;
out vec3 vWorldPosition;
out vec3 vWorldNormal;
out vec3 vWorldTangent;
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;
vWorldTangent = mat3(ModelIT) * tangent;
vUv = uv;
}
タンジェントをフラグメントシェーダに渡しています。
フラグメントシェーダを次のように変更します。
shader.frag
#version 460
in vec3 vWorldPosition;
in vec3 vWorldNormal;
in vec3 vWorldTangent;in vec2 vUv;
layout (location = 0) out vec4 fragment;
uniform vec3 worldCameraPosition;
layout (binding = 0) uniform sampler2D albedoMap;
layout (binding = 1) uniform sampler2D normalMap;
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;
vec3 GetNormal() { vec3 normal = normalize(vWorldNormal); vec3 bitangent = normalize(cross(normal, normalize(vWorldTangent))); vec3 tangent = normalize(cross(bitangent, normal)); mat3 TBN = mat3(tangent, bitangent, normal); vec3 normalFromMap = texture(normalMap, vUv).rgb * 2 - 1; return normalize(TBN * normalFromMap);}
void main() {
vec3 Kdiff = texture(albedoMap, vUv).rgb;
vec3 L = normalize(worldLightPosition - vWorldPosition);
vec3 V = normalize(worldCameraPosition - vWorldPosition);
vec3 N = GetNormal(); 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);
}
法線マップからワールド法線を復元するGetNormal
関数を作成しました。
Applicationクラスの変更点
Application::Init
内で読み込むメッシュを変更しました。
application.cpp
// Meshの読み込み
auto mesh = Mesh::LoadObjMesh("sphere.obj");
Material作成時に法線マップも渡します。
application.cpp
// Materialの作成
auto material = std::make_shared<Material>(Texture("albedo.png", true),
Texture("normal.png", false));
カメラがちょっと遠かったので近づけました。
application.cpp
// Cameraの作成
camera_ = std::make_unique<Camera>(
glm::vec3(0.0f, 0.0f, 5.0f), glm::vec3(0.0f), glm::radians(60.0f),
static_cast<float>(width) / height, 0.1f, 100.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());
glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, mesh_entity.material_->normal_map_.GetTextureId());
mesh_entity.mesh_->Draw();
}
実行結果
実行結果は次のようになります。
プログラム全文
プログラム全文はGitHubにアップロードしてあります。