OpenGLでDeferredシェーディングを実装する(Geometry Pass)

はじめに

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

この記事ではPBRの古典的Deferredシェーディング用のGeometry Passを実装するところまでを行います。

2020 04 06 12 16 25

Deferredシェーディング(遅延シェーディング)とは

Deferredシェーディング、あるいはDeferredレンダリングとは、すべての可視性テストとサーフェスプロパティの評価を行い、その後別途ライティング計算を行うものです。

前回まで書いていたコードはDeferredレンダリングではなくForwardレンダリングでした。 つまり、三角形をパイプラインに送り出したら、そのシェーディングの値で画面上のイメージが更新されました。

遅延レンダリングでは最初のパスでライティングに必要なサーフェスのマテリアルのパラメータをバッファーに格納します。 このバッファーをG-バッファー(ジオメトリックバッファーの略)と呼びます。 G-バッファーにはライティングの計算に必要なフラグメントの位置や法線の情報、albedo、metallic、roughnessなどのマテリアルの情報を書き出します。

その後のパスで、ライトの影響を計算していきます。

概念だけ述べられてもなんだかよくわからないような気がするかもですが、このマルチパスのレンダリングについてはこのあとの実装していくコードを追っていくと理解がしやすいと思います。

遅延レンダリングの利点は、ライト計算を効率的に行えることです。 大量のライトを使いたい場合はDeferredが良い選択肢となります。

一方で、Deferredライティングには、半透明に弱く、マテリアルの種類を増やすのも難しく、アンチエイリアシングでMSAAを使えない、などの弱点も多々あります。

古典的Deferredシェーディング

Deferredレンダリングは進化を続けていて、新しい手法ではタイル遅延シェーディングなどもあります。 それら新しい遅延レンダリングと対比して、シンプルな素朴な実装の遅延シェーディングを古典的Deferredレンダリングとも呼ぶようです?

この記事では古典的なDeferredレンダリングを実装していきます。

参考資料

Deferred周り含めて次の資料などがとても参考になります。

今回使うモデル

前々回のモデル・テクスチャを使います。

1

2020 04 05 12 35 32

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

前回からの変更点を次に示します。

シーン関連のプログラムコードをsceneに格納する

最初に遅延レンダリングとはあまり関係ありませんが、プログラムコードの整理をします。 プログラムファイルの数が増えてきたので、シーン関連のものをscene/以下に入れます。

2020 04 06 09 54 03

2020 04 06 09 55 18

Cameraクラスの変更

Camera::GetNearCamera::GetFarを追加します。

camera.h
  /**
   * @brief カメラのnearの値を取得する
   * @return カメラのnearの値
   */
  const GLfloat GetNear() const;

  /**
   * @brief カメラのfarの値を取得する
   * @return カメラのfarの値
   */
  const GLfloat GetFar() const;

Camera::GetNearCamera::GetFarの実装のためにnearとfarを保持するメンバ変数をもたせます。

camera.h
 private:
  GLfloat near_;  GLfloat far_;  glm::vec3 position_;
  glm::vec3 rotation_;
  glm::mat4 view_matrix_;
  glm::mat4 projection_matrix_;

Camera::GetNearCamera::GetFarの実装は次の通り。

camera.cpp
const GLfloat Camera::GetNear() const { return near_; }

const GLfloat Camera::GetFar() const { return far_; }

Camera::Perspectiveをnearとfarを保存するように変更します。

camera.cpp
void Camera::Perspective(const GLfloat fovy, const GLfloat aspect,
                         const GLfloat near, const GLfloat far) {
  near_ = near;  far_ = far;  projection_matrix_ = glm::perspective(fovy, aspect, near, far);
}

コンストラクタでもnearとfarを保存するように変更します。

camera.cpp
Camera::Camera(const glm::vec3 position, const glm::vec3 rotation,
               const GLfloat fovy, const GLfloat aspect, const GLfloat near,
               const GLfloat far)
    : near_(near),      far_(far),      position_(position),
      rotation_(rotation),
      view_matrix_(),
      projection_matrix_(glm::perspective(fovy, aspect, near, far)) {
  RecaluculateViewMatirx();
}

Sceneクラスの作成

これまでシーン情報はApplication::Init内で構築していましたが、今後複雑なシーンを作っていくことを考えてシーンを管理するsceneクラスを作成します。

scene.h
#ifndef OPENGL_PBR_MAP_SCENE_H_
#define OPENGL_PBR_MAP_SCENE_H_

#include <glm/glm.hpp>
#include <memory>
#include <vector>

#include "camera.h"
#include "mesh.h"
#include "mesh_entity.h"

namespace game {

class Scene {
 public:
  std::unique_ptr<Camera> camera_;
  std::vector<MeshEntity> mesh_entities_;

  /**
   * @brief テスト用のシーンを作成する
   * @param width ウィンドウの幅
   * @param height ウィンドウの高さ
   * @return テスト用シーン
   */
  static std::unique_ptr<Scene> CreateTestScene(const int width,
                                                const int height);
};

}  // namespace game

#endif  // OPENGL_PBR_MAP_SCENE_H_
scene.cpp
#include "scene.h"

namespace game {

std::unique_ptr<Scene> Scene::CreateTestScene(const int width,
                                              const int height) {
  // Meshの読み込み
  auto mesh = Mesh::LoadObjMesh("monkey.obj");

  // Materialの作成
  auto material =
      std::make_shared<Material>(Texture("monkey_BaseColor.png", true),
                                 Texture("monkey_Metallic.png", false),
                                 Texture("monkey_Roughness.png", false),
                                 Texture("monkey_Normal.png", false),
                                 Texture("monkey_Emissive.png", true), 10.0f);

  // Sceneの作成
  auto scene = std::make_unique<Scene>();

  // Cameraの追加
  scene->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);

  // MeshEntityの追加
  scene->mesh_entities_.emplace_back(mesh, material,
                                     glm::vec3(-2.0f, 0.0f, 0.0f),
                                     glm::vec3(0.0f), glm::vec3(1.0f));
  scene->mesh_entities_.emplace_back(mesh, material,
                                     glm::vec3(0.0f, 0.0f, 0.0f),
                                     glm::vec3(0.0f), glm::vec3(1.0f));
  scene->mesh_entities_.emplace_back(mesh, material,
                                     glm::vec3(2.0f, 0.0f, 0.0f),
                                     glm::vec3(0.0f), glm::vec3(1.0f));

  return scene;
}

}  // namespace game

とりあえず現在はMeshEntityの列とCameraを保持するクラスとします。

前回までApplication::Initで構築していたシーンの情報の作成をScene::CreateTestSceneに移動しました。

SceneRendererクラスの作成

これまでレンダリングパスはApplicationクラスの中に直接書いていました。 今後、レンダリングパスが複雑になることを考えて、シーンのレンダリング処理を閉じ込めたSceneRendererクラスを作成します。

scene_renderer.h
#ifndef OPENGL_PBR_MAP_SCENE_RENDERER_H_
#define OPENGL_PBR_MAP_SCENE_RENDERER_H_

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

#include "geometry_pass.h"
#include "scene.h"

namespace game {

/**
 * @brief レンダラーのクラス
 *
 * シーンのレンダリングを行います。
 * 古典的なDeferredレンダリングで実装しています。
 * リソースの多重開放を防ぐためにコピー禁止です。
 */
class SceneRenderer {
 public:
  /**
   * @brief メインのレンダリング処理
   * @param scene レンダリングするシーン
   * @param delta_time 前フレームからの時間(秒)
   */
  void Render(const Scene& scene, const double delta_time);

  /**
   * @brief コンストラクタ
   * @param width ウィンドウの幅
   * @param height ウィンドウの高さ
   */
  SceneRenderer(const GLuint width, const GLuint height);

  /**
   * @brief デストラクタ
   *
   * コンストラクタで生成したフレームバッファオブジェクトを開放します。
   */
  ~SceneRenderer();

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

 private:
  const GLuint width_;
  const GLuint height_;

  const GLuint gbuffer0_;       // rgb: albedo, a: metallic
  const GLuint gbuffer1_;       // rgb: emissive, a: depth
  const GLuint gbuffer2_;       // rgb: normal, a: roughenss
  const GLuint gbuffer_depth_;  // depth buffer
  const GLuint gbuffer_fbo_;

  GeometryPass geometry_pass_;

  /**
   * @brief リソースの開放をする
   */
  void Release();

  /**
   * @brief GBuffer0のテクスチャを作成する
   * @param width ウィンドウの幅
   * @param height ウィンドウの高さ
   * @return 作成したGBuffer0テクスチャのID
   */
  static const GLuint CreateGBuffer0(const GLuint width, const GLuint height);

  /**
   * @brief GBuffer1のテクスチャを作成する
   * @param width ウィンドウの幅
   * @param height ウィンドウの高さ
   * @return 作成したGBuffer1テクスチャのID
   */
  static const GLuint CreateGBuffer1(const GLuint width, const GLuint height);

  /**
   * @brief GBuffer2のテクスチャを作成する
   * @param width ウィンドウの幅
   * @param height ウィンドウの高さ
   * @return 作成したGBuffer2テクスチャのID
   */
  static const GLuint CreateGBuffer2(const GLuint width, const GLuint height);

  /**
   * @brief GBufferのデプステクスチャを作成する
   * @param width ウィンドウの幅
   * @param height ウィンドウの高さ
   * @return 作成したGBufferのデプステクスチャのID
   */
  static const GLuint CreateGBufferDepth(const GLuint width,
                                         const GLuint height);

  /**
   * @brief GBufferのFBOを作成する
   * @param gbuffer0 GBuffer0のテクスチャID
   * @param gbuffer1 GBuffer1のテクスチャID
   * @param gbuffer2 GBuffer2のテクスチャID
   * @param gbuffer_depth GBuffer DepthのテクスチャID
   * @return 作成したFBOのID
   */
  static const GLuint CreateGBufferFbo(const GLuint gbuffer0,
                                       const GLuint gbuffer1,
                                       const GLuint gbuffer2,
                                       const GLuint gbuffer_depth);
};

}  // namespace game

#endif  // OPENGL_PBR_MAP_SCENE_RENDERER_H_
scene_renderer.cpp
#include "scene_renderer.h"

namespace game {

void SceneRenderer::Render(const Scene& scene, const double delta_time) {
  geometry_pass_.Render(scene);

  glBindFramebuffer(GL_READ_FRAMEBUFFER, gbuffer_fbo_);
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
  glReadBuffer(GL_COLOR_ATTACHMENT0);
  // glReadBuffer(GL_COLOR_ATTACHMENT1);
  // glReadBuffer(GL_COLOR_ATTACHMENT2);
  glBlitFramebuffer(0, 0, width_, height_, 0, 0, width_, height_,
                    GL_COLOR_BUFFER_BIT, GL_NEAREST);
}

SceneRenderer::SceneRenderer(const GLuint width, const GLuint height)
    : width_(width),
      height_(height),
      gbuffer0_(CreateGBuffer0(width, height)),
      gbuffer1_(CreateGBuffer1(width, height)),
      gbuffer2_(CreateGBuffer2(width, height)),
      gbuffer_depth_(CreateGBufferDepth(width, height)),
      gbuffer_fbo_(
          CreateGBufferFbo(gbuffer0_, gbuffer1_, gbuffer2_, gbuffer_depth_)),
      geometry_pass_(gbuffer_fbo_) {}

SceneRenderer::~SceneRenderer() { Release(); }

void SceneRenderer::Release() {
  glDeleteFramebuffers(1, &gbuffer_fbo_);
  glDeleteTextures(1, &gbuffer0_);
  glDeleteTextures(1, &gbuffer1_);
  glDeleteTextures(1, &gbuffer2_);
  glDeleteRenderbuffers(1, &gbuffer_depth_);
}

const GLuint SceneRenderer::CreateGBuffer0(const GLuint width,
                                           const GLuint height) {
  GLuint gbuffer0;
  glGenTextures(1, &gbuffer0);
  glBindTexture(GL_TEXTURE_2D, gbuffer0);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA,
               GL_UNSIGNED_BYTE, nullptr);
  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_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  glBindTexture(GL_TEXTURE_2D, 0);
  return gbuffer0;
}

const GLuint SceneRenderer::CreateGBuffer1(const GLuint width,
                                           const GLuint height) {
  GLuint gbuffer1;
  glGenTextures(1, &gbuffer1);
  glBindTexture(GL_TEXTURE_2D, gbuffer1);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA,
               GL_UNSIGNED_BYTE, nullptr);
  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_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  glBindTexture(GL_TEXTURE_2D, 0);
  return gbuffer1;
}

const GLuint SceneRenderer::CreateGBuffer2(const GLuint width,
                                           const GLuint height) {
  GLuint gbuffer2;
  glGenTextures(1, &gbuffer2);
  glBindTexture(GL_TEXTURE_2D, gbuffer2);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA,
               GL_UNSIGNED_BYTE, nullptr);
  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_MIN_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  glBindTexture(GL_TEXTURE_2D, 0);
  return gbuffer2;
}

const GLuint SceneRenderer::CreateGBufferDepth(const GLuint width,
                                               const GLuint height) {
  GLuint gbuffer_depth_buffer;
  glGenRenderbuffers(1, &gbuffer_depth_buffer);
  glBindRenderbuffer(GL_RENDERBUFFER, gbuffer_depth_buffer);
  glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);
  glBindRenderbuffer(GL_RENDERBUFFER, 0);
  return gbuffer_depth_buffer;
}

const GLuint SceneRenderer::CreateGBufferFbo(const GLuint gbuffer0,
                                             const GLuint gbuffer1,
                                             const GLuint gbuffer2,
                                             const GLuint gbuffer_depth) {
  GLuint gbuffer_fbo;
  glGenFramebuffers(1, &gbuffer_fbo);
  glBindFramebuffer(GL_FRAMEBUFFER, gbuffer_fbo);
  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
                         gbuffer0, 0);
  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D,
                         gbuffer1, 0);
  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D,
                         gbuffer2, 0);
  glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
                            GL_RENDERBUFFER, gbuffer_depth);
  glBindFramebuffer(GL_FRAMEBUFFER, 0);
  return gbuffer_fbo;
}

}  // namespace game

クラス中に出てくるGeometryPassクラスは後で述べます。

Renderメソッドがレンダリング処理のメソッドです。 今回はGeometryPassの結果をそのままデフォルトフレームバッファにBlitしています。 コメントアウトしてある部分を切り替えることで表示するG-Bufferを変更できます。

このクラスではG-Bufferを含むレンダリングに必要になるバッファを保持します。 今回のG-Bufferの構成はコメントにある通り、G-Buffer 0のrgbをalbedo、aをmetallic、G-Buffer 1のrgbをemissive、aをdepth、G-Buffer 2のrgbをnormal、aをroughnessとします。 G-Buffer 1はHDRなバッファとします。

コンストラクタで、それぞれのG-Buffer用のテクスチャを作成して、そのテクスチャに書き込むためのFBOを作成しています。

CreateProgram関数の作成

いままでapplication.cppにとりあえず書いてあったCreateProgram関数を、createprogram.hとcreateprogram.cppを作成しそちらに移動します。

create_program.h
#ifndef OPENGL_PBR_MAP_CREATE_PROGRAM_H_
#define OPENGL_PBR_MAP_CREATE_PROGRAM_H_

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

#include <fstream>
#include <iostream>
#include <string>
#include <vector>

namespace game {

/**
 * @brief シェーダを読み込む
 * @param vertex_shader_pass 頂点シェーダのパス
 * @param fragment_shader_pass フラグメントシェーダのパス
 */
GLuint CreateProgram(const std::string& vertex_shader_pass,
                     const std::string& fragment_shader_pass);

}  // namespace game

#endif  // OPENGL_PBR_MAP_CREATE_PROGRAM_H_
create_program.cpp
#include "create_program.h"

namespace game {

const GLuint CreateProgram(const std::string& vertex_shader_pass,
                           const std::string& fragment_shader_pass) {
  // 頂点シェーダの読み込み
  std::ifstream vertex_ifs(vertex_shader_pass, std::ios::binary);
  if (vertex_ifs.fail()) {
    std::cerr << "Error: Can't open source file: " << vertex_shader_pass
              << std::endl;
    return 0;
  }
  auto vertex_shader_source =
      std::string(std::istreambuf_iterator<char>(vertex_ifs),
                  std::istreambuf_iterator<char>());
  if (vertex_ifs.fail()) {
    std::cerr << "Error: could not read source file: " << vertex_shader_pass
              << std::endl;
    return 0;
  }
  GLchar const* vertex_shader_source_pointer = vertex_shader_source.c_str();

  // フラグメントシェーダの読み込み
  std::ifstream fragment_ifs(fragment_shader_pass, std::ios::binary);
  if (fragment_ifs.fail()) {
    std::cerr << "Error: Can't open source file: " << fragment_shader_pass
              << std::endl;
    return 0;
  }
  auto fragment_shader_source =
      std::string(std::istreambuf_iterator<char>(fragment_ifs),
                  std::istreambuf_iterator<char>());
  if (fragment_ifs.fail()) {
    std::cerr << "Error: could not read source file: " << fragment_shader_pass
              << std::endl;
    return 0;
  }
  GLchar const* fragment_shader_source_pointer = fragment_shader_source.c_str();

  // プログラムオブジェクトを作成
  const GLuint program = glCreateProgram();

  GLint status = GL_FALSE;
  GLsizei info_log_length;

  // 頂点シェーダのコンパイル
  const GLuint vertex_shader_obj = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(vertex_shader_obj, 1, &vertex_shader_source_pointer, nullptr);
  glCompileShader(vertex_shader_obj);
  glAttachShader(program, vertex_shader_obj);

  // 頂点シェーダのチェック
  glGetShaderiv(vertex_shader_obj, GL_COMPILE_STATUS, &status);
  if (status == GL_FALSE)
    std::cerr << "Compile Error in Vertex Shader." << std::endl;
  glGetShaderiv(vertex_shader_obj, GL_INFO_LOG_LENGTH, &info_log_length);
  if (info_log_length > 1) {
    std::vector<GLchar> vertex_shader_error_message(info_log_length);
    glGetShaderInfoLog(vertex_shader_obj, info_log_length, nullptr,
                       vertex_shader_error_message.data());
    std::cerr << vertex_shader_error_message.data() << std::endl;
  }

  glDeleteShader(vertex_shader_obj);

  // フラグメントシェーダのコンパイル
  const GLuint fragment_shader_obj = glCreateShader(GL_FRAGMENT_SHADER);
  glShaderSource(fragment_shader_obj, 1, &fragment_shader_source_pointer,
                 nullptr);
  glCompileShader(fragment_shader_obj);
  glAttachShader(program, fragment_shader_obj);

  // フラグメントシェーダのチェック
  glGetShaderiv(fragment_shader_obj, GL_COMPILE_STATUS, &status);
  if (status == GL_FALSE)
    std::cerr << "Compile Error in Fragment Shader." << std::endl;
  glGetShaderiv(fragment_shader_obj, GL_INFO_LOG_LENGTH, &info_log_length);
  if (info_log_length > 1) {
    std::vector<GLchar> fragment_shader_error_message(info_log_length);
    glGetShaderInfoLog(fragment_shader_obj, info_log_length, nullptr,
                       fragment_shader_error_message.data());
    std::cerr << fragment_shader_error_message.data() << std::endl;
  }

  glDeleteShader(fragment_shader_obj);

  // プログラムのリンク
  glLinkProgram(program);

  // リンクのチェック
  glGetProgramiv(program, GL_LINK_STATUS, &status);
  if (status == GL_FALSE) std::cerr << "Link Error." << std::endl;
  glGetProgramiv(program, GL_INFO_LOG_LENGTH, &info_log_length);
  if (info_log_length > 1) {
    std::vector<GLchar> program_link_error_message(info_log_length);
    glGetProgramInfoLog(program, info_log_length, nullptr,
                        program_link_error_message.data());
    std::cerr << program_link_error_message.data() << std::endl;
  }

  return program;
}

}  // namespace game

GeometryPassクラスの作成

GeometryPassクラスを作成します。

geometry_pass.h
#ifndef OPENGL_PBR_MAP_GEOMETRY_PASS_H_
#define OPENGL_PBR_MAP_GEOMETRY_PASS_H_

#include <glm/ext.hpp>
#include <glm/glm.hpp>

#include "create_program.h"
#include "scene.h"

namespace game {

class GeometryPass {
 public:
  /**
   * @brief このパスをレンダリングする
   * @param scene レンダリングするシーン
   */
  void Render(const Scene& sccene);

  /**
   * @brief コンストラクタ
   * @param gbuffer_fbo GBufferのFBO
   */
  GeometryPass(const GLuint gbuffer_fbo);

  /**
   * @brief デストラクタ
   *
   * コンストラクタで確保したシェーダプログラムを開放します。
   */
  ~GeometryPass();

 private:
  const GLuint gbuffer_fbo_;

  const GLuint shader_program_;

  const GLuint model_loc_;
  const GLuint model_it_loc_;
  const GLuint view_loc_;
  const GLuint projection_loc_;
  const GLuint projection_params_loc_;
  const GLuint emissive_intensity_loc_;
};

}  // namespace game

#endif  // OPENGL_PBR_MAP_GEOMETRY_PASS_H_
geometry_pass.cpp
#include "geometry_pass.h"

namespace game {

void GeometryPass::Render(const Scene& scene) {
  glEnable(GL_STENCIL_TEST);

  glStencilFunc(GL_ALWAYS, 128, 128);
  glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
  glStencilMask(255);
  glDepthMask(GL_TRUE);
  glEnable(GL_DEPTH_TEST);
  glDisable(GL_BLEND);

  glUseProgram(shader_program_);
  glBindFramebuffer(GL_FRAMEBUFFER, gbuffer_fbo_);

  const GLenum bufs[] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1,
                         GL_COLOR_ATTACHMENT2};
  glDrawBuffers(3, bufs);

  glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
  glClearDepth(1.0);
  glClearStencil(0.0);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

  const auto view = scene.camera_->GetViewMatrix();
  const auto projection = scene.camera_->GetProjectionMatrix();
  const auto projection_params =
      glm::vec2(scene.camera_->GetNear(), scene.camera_->GetFar());

  glUniformMatrix4fv(view_loc_, 1, GL_FALSE, &view[0][0]);
  glUniformMatrix4fv(projection_loc_, 1, GL_FALSE, &projection[0][0]);
  glUniform2fv(projection_params_loc_, 1, &projection_params[0]);

  for (const auto& mesh_entity : scene.mesh_entities_) {
    const auto model = mesh_entity.GetModelMatrix();
    const 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_->metallic_map_.GetTextureId());
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D,
                  mesh_entity.material_->roughness_map_.GetTextureId());
    glActiveTexture(GL_TEXTURE3);
    glBindTexture(GL_TEXTURE_2D,
                  mesh_entity.material_->normal_map_.GetTextureId());
    glActiveTexture(GL_TEXTURE4);
    glBindTexture(GL_TEXTURE_2D,
                  mesh_entity.material_->emissive_map_.GetTextureId());

    glUniform1fv(emissive_intensity_loc_, 1,
                 &mesh_entity.material_->emissive_intensity_);

    mesh_entity.mesh_->Draw();
  }
}

GeometryPass::GeometryPass(const GLuint gbuffer_fbo)
    : gbuffer_fbo_(gbuffer_fbo),
      shader_program_(CreateProgram("shader/GeometryPass.vert",
                                    "shader/GeometryPass.frag")),
      model_loc_(glGetUniformLocation(shader_program_, "Model")),
      model_it_loc_(glGetUniformLocation(shader_program_, "ModelIT")),
      view_loc_(glGetUniformLocation(shader_program_, "View")),
      projection_loc_(glGetUniformLocation(shader_program_, "Projection")),
      projection_params_loc_(
          glGetUniformLocation(shader_program_, "ProjectionParams")),
      emissive_intensity_loc_(
          glGetUniformLocation(shader_program_, "emissiveIntensity")) {}

GeometryPass::~GeometryPass() { glDeleteProgram(shader_program_); }

}  // namespace game

コンストラクタではシェーダプログラムの作成とUniform Locationの取得を行っています。

GeometryPass::Render関数では、最初にフレームの最初での設定を行っています。

まずはステンシルの設定です。 描画した領域をステンシル128のビットを立てています。 後で描画していない領域に空を描画したり、Directional Lightのライティングの必要なピクセルの範囲を指定するのに使います。

  glStencilFunc(GL_ALWAYS, 128, 128);
  glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
  glStencilMask(255);

次にDepthを有効にし、Blendを無効にしています。

  glDepthMask(GL_TRUE);
  glEnable(GL_DEPTH_TEST);
  glDisable(GL_BLEND);

次にGeometryPassで使うシェーダプログラムの指定と書き込み先の指定を行っています。 MRTで3つのバッファに同時に書き込みます。

  glUseProgram(shader_program_);
  glBindFramebuffer(GL_FRAMEBUFFER, gbuffer_fbo_);

  const GLenum bufs[] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1,
                         GL_COLOR_ATTACHMENT2};
  glDrawBuffers(3, bufs);

その後バッファのクリアを行います。

  glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
  glClearDepth(1.0);
  glClearStencil(0.0);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

その後、すべてのMeshEntityで変わらないViewやProjectionのUniform変数を渡しています。

  const auto view = scene.camera_->GetViewMatrix();
  const auto projection = scene.camera_->GetProjectionMatrix();
  const auto projection_params =
      glm::vec2(scene.camera_->GetNear(), scene.camera_->GetFar());

  glUniformMatrix4fv(view_loc_, 1, GL_FALSE, &view[0][0]);
  glUniformMatrix4fv(projection_loc_, 1, GL_FALSE, &projection[0][0]);
  glUniform2fv(projection_params_loc_, 1, &projection_params[0]);

その後、MeshEntityについてループを回して描画を行っていきます。

  for (const auto& mesh_entity : scene.mesh_entities_) {
    const auto model = mesh_entity.GetModelMatrix();
    const 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_->metallic_map_.GetTextureId());
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D,
                  mesh_entity.material_->roughness_map_.GetTextureId());
    glActiveTexture(GL_TEXTURE3);
    glBindTexture(GL_TEXTURE_2D,
                  mesh_entity.material_->normal_map_.GetTextureId());
    glActiveTexture(GL_TEXTURE4);
    glBindTexture(GL_TEXTURE_2D,
                  mesh_entity.material_->emissive_map_.GetTextureId());

    glUniform1fv(emissive_intensity_loc_, 1,
                 &mesh_entity.material_->emissive_intensity_);

    mesh_entity.mesh_->Draw();
  }

シェーダの作成

shader/GeometryPass.vertを作成します。

shader/GeometryPass.vert
#version 460

uniform mat4 Model;
uniform mat4 ModelIT;
uniform mat4 View;
uniform mat4 Projection;
uniform vec2 ProjectionParams; // x: near, y: far

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 vWorldNormal;
out vec3 vWorldTangent;
out float vDepth;
out vec2 vUv;

float EncodeDepth(in float viewDepth, in vec2 ProjectionParams)
{
  return (-viewDepth - ProjectionParams.x) / (ProjectionParams.y - ProjectionParams.x);
}

void main()
{
  vWorldNormal = mat3(ModelIT) * normal;
  vWorldTangent = mat3(ModelIT) * tangent;
  vUv = uv;

  vec4 viewPos = View * Model * position;
  vDepth = EncodeDepth(viewPos.z, ProjectionParams);

  gl_Position = Projection * viewPos;
}

基本的に必要な情報を計算しフラグメントシェーダに送っているだけです。

Depthのエンコードがちょっと特殊ですね。 Projection行列を掛けるのではなく、nearとfarの値を利用して線形に0-1の範囲に収めています。

shader/GeometryPass.fragを作成します。

shader/GeometryPass.frag
#version 460

in vec3 vWorldNormal;
in vec3 vWorldTangent;
in float vDepth;
in vec2 vUv;

layout (location = 0) out vec4 GBuffer0; // rgb: albedo, a: metallic
layout (location = 1) out vec4 GBuffer1; // rgb: emissive, a: depth
layout (location = 2) out vec4 GBuffer2; // rgb: world normal, a: roughness

layout (binding = 0) uniform sampler2D albedoMap;
layout (binding = 1) uniform sampler2D metallicMap;
layout (binding = 2) uniform sampler2D roughnessMap;
layout (binding = 3) uniform sampler2D normalMap;
layout (binding = 4) uniform sampler2D emissiveMap;

uniform float emissiveIntensity;


const float PI = 3.14159265358979323846;

const mat3 sRGB_2_AP0 = mat3(
  0.4397010, 0.0897923, 0.0175440,
  0.3829780, 0.8134230, 0.1115440,
  0.1773350, 0.0967616, 0.8707040
);


vec3 sRGBToACES(vec3 srgb)
{
  return sRGB_2_AP0 * srgb;
}

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 albedo = sRGBToACES(texture(albedoMap, vUv).rgb);
  float metallic = texture(metallicMap, vUv).r;
  float roughness = texture(roughnessMap, vUv).r;
  vec3 normal = GetNormal();
  vec3 emissive = sRGBToACES(texture(emissiveMap, vUv).rgb) * emissiveIntensity;

  GBuffer0 = vec4(albedo, metallic);
  GBuffer1 = vec4(emissive, vDepth);
  GBuffer2 = vec4(normal * 0.5 + 0.5, roughness);
}

テクスチャを引っ張ってきてG-Bufferに格納しているだけです。 色情報についてはsRGBからACES色空間に変換しています。

Applicationクラスの変更

インクルードするヘッダを変更します。

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

#include <glm/ext.hpp>
#include <glm/glm.hpp>
#include <iostream>
#include <memory>
#include <string>

#include "scene.h"#include "scene_renderer.h"

SceneとSceneRenderer用のメンバ変数を用意します。 前回までで使っていたメンバ変数で不要なものは消します。

application.h
 private:
  GLFWwindow* window_;
  std::unique_ptr<Scene> scene_;  std::unique_ptr<SceneRenderer> scene_renderer_;

Application::Initは次のように変更します。

application.cpp
bool Application::Init() {
  const GLuint width = 960;
  const GLuint height = 540;

  if (!InitWindow(width, height)) {
    std::cerr << "Error: InitWindow" << std::endl;
    return false;
  }

  // Sceneの作成  scene_ = Scene::CreateTestScene(width, height);
  // SceneRendererの作成  scene_renderer_ = std::make_unique<SceneRenderer>(width, height);
  return true;
}

Application::Updateは次のように変更します。

application.cpp
void Application::Update(const double delta_time) {
  scene_renderer_->Render(*scene_, delta_time);
}

実行結果

実行結果は次のとおりです。

G-Buffer 0を表示した場合。

2020 04 06 12 15 18

albedoが表示されています。

G-Buffer 1を表示した場合。

2020 04 06 12 15 51

emissive項が表示されています。

G-Buffer 2を表示した場合。

2020 04 06 12 16 25

normalが表示されています。

プログラム全文

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

GitHub: MatchaChoco010/OpenGL-PBR-Map at v10