OpenGLで法線マップを試してみる

はじめに

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

この記事では法線マップを使って球を表示するところまでを行います。

screenshot 0

モデルと法線マップの用意

法線マップの効果をわかりやすくするために猿ではなく球を使うことにします。

screenshot 1

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

normal.png

次のテクスチャをalbedo.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);
}

タンジェントの計算については次のページを参考にしました。

チュートリアル13:法線マッピング

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();
  }

実行結果

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

screenshot 0

プログラム全文

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

GitHub: MatchaChoco010/OpenGL-PBR-Map at v7