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

はじめに

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

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

2020 04 06 19 21 38

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

前回からのプログラムの変更点を次に示します。

DirectionalLightクラスの作成

DirectionalLightクラスを作成します。

directional_light.h
#ifndef OPENGL_PBR_MAP_DIRECTIONAL_LIGHT_H_
#define OPENGL_PBR_MAP_DIRECTIONAL_LIGHT_H_

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

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

namespace game {

/**
 * @brief 平行光源を表現するオブジェクト
 *
 * 平行光源の各種プロパティを保持し、影の計算用の行列を扱うクラスです。
 * Directional Lightはシーンに一つのみ含まれます。
 */
class DirectionalLight {
 public:
  /**
   * @brief 光源の強さを取得する
   * @return 光源の強さ(lx)
   *
   * 平行光源の強さはlx(ルクス)で表現します。
   */
  const GLfloat GetIntensity() const;

  /**
   * @brief 光源の強さを設定する
   * @param intensity 新しい光源の強さ(lx)
   *
   * 平行光源の強さはlx(ルクス)で表現します。
   */
  void SetIntensity(const GLfloat intensity);

  /**
   * @brief 平行光源の向きを取得する
   * @return 平行光源の向きのベクトル
   *
   * 正規化された向きベクトルを返します。
   */
  const glm::vec3 GetDirection() const;

  /**
   * @brief 平行光源の向きを設定する
   * @param direction 新しい平行光源の向きのベクトル
   *
   * 渡されたベクトルは正規化され保持されます。
   * 内部で保持する影行列を再計算します。
   */
  void SetDirection(const glm::vec3 direction);

  /**
   * @brief 平行光源の色を取得する
   * @return 平行光源の色
   */
  const glm::vec3 GetColor() const;

  /**
   * @brief 平行光源の色を設定する
   * @param color 新しい平行光源の色
   */
  void SetColor(const glm::vec3 color);

  /**
   * @brief コンストラクタ
   * @param intensity 光源の強さ(lx)
   * @param direction 光源の向きベクトル
   * @param color 光源の色
   */
  DirectionalLight(const GLfloat intensity, const glm::vec3 direction,
                   const glm::vec3 color);

 private:
  GLfloat intensity_;
  glm::vec3 direction_;
  glm::vec3 color_;
};

}  // namespace game

#endif  // OPENGL_PBR_MAP_DIRECTIONAL_LIGHT_H_
directional_light.cpp
#include "directional_light.h"

namespace game {
const GLfloat DirectionalLight::GetIntensity() const { return intensity_; }

void DirectionalLight::SetIntensity(const GLfloat intensity) {
  intensity_ = intensity;
}

const glm::vec3 DirectionalLight::GetDirection() const { return direction_; }

void DirectionalLight::SetDirection(const glm::vec3 direction) {
  direction_ = glm::normalize(direction);
}

const glm::vec3 DirectionalLight::GetColor() const { return color_; }

void DirectionalLight::SetColor(const glm::vec3 color) { color_ = color; }

DirectionalLight::DirectionalLight(const GLfloat intensity,
                                   const glm::vec3 direction,
                                   const glm::vec3 color)
    : intensity_(intensity),
      direction_(glm::normalize(direction)),
      color_(color) {}

}  // namespace game

Direction、Intensity、Colorを保持するクラスです。

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 "directional_light.h"#include "mesh.h"
#include "mesh_entity.h"

namespace game {

class Scene {
 public:
  std::unique_ptr<Camera> camera_;
  std::vector<MeshEntity> mesh_entities_;
  std::unique_ptr<DirectionalLight> directional_light_;
  /**
   * @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
  // 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));

  // DirectionalLightの設定  scene->directional_light_ = std::make_unique<DirectionalLight>(      700.0f, gl ::vec3(-0.5f, -1.0f, -0.5f), glm::vec3(1.0f));
  return scene;

ライトの強さを適当に700luxに設定しています。

DirectionalLightPassクラスの作成

DirectionalLightPassクラスを作成します。

directional_light_pass.h
#ifndef OPENGL_PBR_MAP_DIRECTIONAL_LIGHT_PASS_H_
#define OPENGL_PBR_MAP_DIRECTIONAL_LIGHT_PASS_H_

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

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

namespace game {

/**
 * @brief DirectionalLighatパスを表現するクラス
 *
 * GBufferとシーンのDirectionalLightの情報をもとに
 * DirectionalLightのライティングを計算しHDRのカラーバッファに書き込みます。
 * emissiveの値も処理します。
 * リソースの多重開放を避けるためコピー禁止です。
 */
class DirectionalLightPass {
 public:
  /**
   * @brief このパスをレンダリングする
   * @param scene レンダリングするシーン
   *
   * DirectionalLightのライティングとemissive項をHDRカラーバッファに書き出します。
   */
  void Render(const Scene& scene) const;

  /**
   * @brief コンストラクタ
   * @param hdr_color_fbo HDRのフレームバッファオブジェクト
   * @param gbuffer0 GBuffer0のテクスチャID
   * @param gbuffer1 GBuffer1のテクスチャID
   * @param gbuffer2 GBuffer2のテクスチャID
   * @param fullscreen_vao 画面を覆むメッシュのVAO
   * @param irradiance_map 放射照度マップのテクスチャID
   * @param width ウィンドウの幅
   * @param height ウィンドウの高さ
   *
   * FBO及びGBufferテクスチャ、VAOの所有権は奪いません。
   * FBO及びGBufferテクスチャ、VAOの開放の責任は外側のスコープです。
   * シャドウマップ用のFBOも確保します。
   */
  DirectionalLightPass(const GLuint hdr_color_fbo, const GLuint gbuffer0,
                       const GLuint gbuffer1, const GLuint gbuffer2,
                       const GLuint fullscreen_vao, const GLuint width,
                       const GLuint height);

  /**
   * @brief デストラクタ
   *
   * コンストラクタで確保したリソースを開放します。
   */
  ~DirectionalLightPass();

 private:
  const GLuint width_;
  const GLuint height_;

  const GLuint hdr_fbo_;
  const GLuint gbuffer0_;
  const GLuint gbuffer1_;
  const GLuint gbuffer2_;
  const GLuint fullscreen_vao_;

  const GLuint shader_program_;
  const GLuint light_direction_loc_;
  const GLuint light_intensity_loc_;
  const GLuint light_color_loc_;
  const GLuint world_camera_pos_loc_;
  const GLuint view_projection_i_loc_;
  const GLuint projection_params_loc_;
};

}  // namespace game

#endif  // OPENGL_PBR_MAP_DIRECTIONAL_LIGHT_PASS_H_
directional_light_pass.cpp
#include "directional_light_pass.h"

namespace game {

void DirectionalLightPass::Render(const Scene& scene) const {
  // Lighting Pass
  glUseProgram(shader_program_);
  glBindFramebuffer(GL_FRAMEBUFFER, hdr_fbo_);

  glClear(GL_COLOR_BUFFER_BIT);

  glStencilFunc(GL_EQUAL, 128, 128);
  glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
  glStencilMask(0);

  glDepthMask(GL_FALSE);
  glDisable(GL_DEPTH_TEST);

  const auto direction = scene.directional_light_->GetDirection();
  const auto intensity = scene.directional_light_->GetIntensity();
  const auto color = scene.directional_light_->GetColor();
  const auto world_camera_pos = scene.camera_->GetPosition();
  const auto view_projection_i =
      glm::inverse(scene.camera_->GetViewProjectionMatrix());
  const auto projection_params =
      glm::vec2(scene.camera_->GetNear(), scene.camera_->GetFar());

  glUniform3fv(light_direction_loc_, 1, &direction[0]);
  glUniform1fv(light_intensity_loc_, 1, &intensity);
  glUniform3fv(light_color_loc_, 1, &color[0]);
  glUniform3fv(world_camera_pos_loc_, 1, &world_camera_pos[0]);
  glUniformMatrix4fv(view_projection_i_loc_, 1, GL_FALSE,
                     &view_projection_i[0][0]);
  glUniform2fv(projection_params_loc_, 1, &projection_params[0]);

  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, gbuffer0_);
  glActiveTexture(GL_TEXTURE1);
  glBindTexture(GL_TEXTURE_2D, gbuffer1_);
  glActiveTexture(GL_TEXTURE2);
  glBindTexture(GL_TEXTURE_2D, gbuffer2_);

  glBindVertexArray(fullscreen_vao_);
  glDrawArrays(GL_TRIANGLES, 0, 3);

  glEnable(GL_BLEND);
  glBlendEquation(GL_FUNC_ADD);
  glBlendFunc(GL_ONE, GL_ONE);
}

DirectionalLightPass::DirectionalLightPass(
    const GLuint hdr_fbo, const GLuint gbuffer0, const GLuint gbuffer1,
    const GLuint gbuffer2, const GLuint fullscreen_vao, const GLuint width,
    const GLuint height)
    : width_(width),
      height_(height),
      hdr_fbo_(hdr_fbo),
      gbuffer0_(gbuffer0),
      gbuffer1_(gbuffer1),
      gbuffer2_(gbuffer2),
      fullscreen_vao_(fullscreen_vao),
      shader_program_(CreateProgram("shader/DirectionalLightPass.vert",
                                    "shader/DirectionalLightPass.frag")),
      light_direction_loc_(
          glGetUniformLocation(shader_program_, "lightDirection")),
      light_intensity_loc_(
          glGetUniformLocation(shader_program_, "lightIntensity")),
      light_color_loc_(glGetUniformLocation(shader_program_, "lightColor")),
      world_camera_pos_loc_(
          glGetUniformLocation(shader_program_, "worldCameraPos")),
      view_projection_i_loc_(
          glGetUniformLocation(shader_program_, "ViewProjectionI")),
      projection_params_loc_(
          glGetUniformLocation(shader_program_, "ProjectionParams")) {}

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

}  // namespace game

コンストラクタではG-Bufferのテクスチャと書き出し先のHDRバッファを受け取り、Uniform Locationを取得しています。

DirectionalLightPass::Renderがこのパスのレンダリングを行うメンバ関数です。

最初にこのパスのシェーダプログラムを有効にして、書き出し先を指定してカラーバッファをクリアしています。

  // Lighting Pass
  glUseProgram(shader_program_);
  glBindFramebuffer(GL_FRAMEBUFFER, hdr_fbo_);

  glClear(GL_COLOR_BUFFER_BIT);

次にステンシルの設定です。 GeometryPassで128に設定したところについて描画を走らせます。

  glStencilFunc(GL_EQUAL, 128, 128);
  glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
  glStencilMask(0);

Depthテストは無効にします。

  glDepthMask(GL_FALSE);
  glDisable(GL_DEPTH_TEST);

次にUniform変数を渡します。

  const auto direction = scene.directional_light_.GetDirection();
  const auto intensity = scene.directional_light_.GetIntensity();
  const auto color = scene.directional_light_.GetColor();
  const auto world_camera_pos = scene.camera_->GetPosition();
  const auto view_projection_i =
      glm::inverse(scene.camera_->GetViewProjectionMatrix());
  const auto projection_params =
      glm::vec2(scene.camera_->GetNear(), scene.camera_->GetFar());

  glUniform3fv(light_direction_loc_, 1, &direction[0]);
  glUniform1fv(light_intensity_loc_, 1, &intensity);
  glUniform3fv(light_color_loc_, 1, &color[0]);
  glUniform3fv(world_camera_pos_loc_, 1, &world_camera_pos[0]);
  glUniformMatrix4fv(view_projection_i_loc_, 1, GL_FALSE,
                     &view_projection_i[0][0]);
  glUniform2fv(projection_params_loc_, 1, &projection_params[0]);

次にG-Bufferのテクスチャをアクティブにします。

  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, gbuffer0_);
  glActiveTexture(GL_TEXTURE1);
  glBindTexture(GL_TEXTURE_2D, gbuffer1_);
  glActiveTexture(GL_TEXTURE2);
  glBindTexture(GL_TEXTURE_2D, gbuffer2_);

その後、フルスクリーンの三角形を描画します。

  glBindVertexArray(fullscreen_vao_);
  glDrawArrays(GL_TRIANGLES, 0, 3);

最後に以降のパスのためにカラーブレンドを加算に設定します。

  glEnable(GL_BLEND);
  glBlendEquation(GL_FUNC_ADD);
  glBlendFunc(GL_ONE, GL_ONE);

SceneRendererクラスの変更

scene_renderer.hを次のように変更します。

まずは読み込むヘッダの追加をします。

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

#include <array>
#include "directional_light_pass.h"#include "geometry_pass.h"
#include "scene.h"

次にHDRバッファを用意し、DirectionalLightPassをメンバ変数としてもたせます。 画面全体を覆う三角形のメッシュも作成します。

scene_renderer.h
 private:
  const GLuint width_;
  const GLuint height_;

  const GLuint fullscreen_mesh_vao_;  const GLuint fullscreen_mesh_vertices_vbo_;  const GLuint fullscreen_mesh_uvs_vbo_;
  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_;

  const GLuint hdr_color_buffer_;  const GLuint hdr_depth_buffer_;  const GLuint hdr_fbo_;
  GeometryPass geometry_pass_;
  DirectionalLightPass directional_light_pass_;

HDRバッファを作成するための静的メンバ関数を用意します。

scene_renderer.h
  /**   * @brief HDRカラーバッファのテクスチャを作成する   * @return 作成したHDRカラーバッファのテクスチャのID   */  static const GLuint CreateHdrColorBuffer(const GLuint width,                                           const GLuint height);  /**   * @brief HDRバッファのデプスバッファテクスチャを作成する   * @return 作成したHDRデプスバッファテクスチャのID   */  static const GLuint CreateHdrDepthBuffer(const GLuint width,                                           const GLuint height);  /**   * @brief HDRバッファのFBOを作成する   * @return 作成したHDRバッファのFBOのID   */  static const GLuint CreateHdrFbo(const GLuint hdr_color_buffer,                                   const GLuint hdr_depth_buffer);

その実装は次の通り。

scene_renderer.cpp
const GLuint SceneRenderer::CreateHdrColorBuffer(const GLuint width,                                                 const GLuint height) {  GLuint hdr_color_buffer;  glGenTextures(1, &hdr_color_buffer);  glBindTexture(GL_TEXTURE_2D, hdr_color_buffer);  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, width, height, 0, GL_RGB,               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 hdr_color_buffer;}const GLuint SceneRenderer::CreateHdrDepthBuffer(const GLuint width,                                                 const GLuint height) {  GLuint hdr_depth_buffer;  glGenTextures(1, &hdr_depth_buffer);  glBindTexture(GL_TEXTURE_2D, hdr_depth_buffer);  glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width, height, 0,               GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, 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);  glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE,                  GL_DEPTH_COMPONENT);  glBindTexture(GL_TEXTURE_2D, 0);  return hdr_depth_buffer;}const GLuint SceneRenderer::CreateHdrFbo(const GLuint hdr_color_buffer,                                         const GLuint hdr_depth_buffer) {  GLuint hdr_fbo;  glGenFramebuffers(1, &hdr_fbo);  glBindFramebuffer(GL_FRAMEBUFFER, hdr_fbo);  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,                         hdr_color_buffer, 0);  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,                         GL_TEXTURE_2D, hdr_depth_buffer, 0);  glBindFramebuffer(GL_FRAMEBUFFER, 0);  return hdr_fbo;}

画面を追う三角形メッシュを作成する静的メンバ関数を用意します。

scene_renderer.h
  /**
   * @brief スクリーンを覆うメッシュのVAOを作成する
   * @return 作成したVAOのID
   */
  static const GLuint CreateFullscreenMeshVao();

  /**
   * @brief スクリーンを覆うメッシュの頂点座標のVBOを作成する
   * @param fullscreen_mesh_vao スクリーンを覆うメッシュのVAOのID
   * @return 作成したVBOのID
   */
  static const GLuint CreateFullscreenMeshVerticesVbo(
      const GLuint fullscreen_mesh_vao);

  /**
   * @brief スクリーンを覆うメッシュのUVのVBOを作成する
   * @param fullscreen_mesh_vao スクリーンを覆うメッシュのVAOのID
   * @return 作成したVBOのID
   */
  static const GLuint CreateFullscreenMeshUvsVbo(
      const GLuint fullscreen_mesh_vao);
scene_renderer.cpp
const GLuint SceneRenderer::CreateFullscreenMeshVao() {
  GLuint fullscreen_mesh_vao;
  glGenVertexArrays(1, &fullscreen_mesh_vao);
  return fullscreen_mesh_vao;
}

const GLuint SceneRenderer::CreateFullscreenMeshVerticesVbo(
    const GLuint fullscreen_mesh_vao) {
  glBindVertexArray(fullscreen_mesh_vao);

  const std::array<glm::vec2, 3> fullscreen_mesh_vertices = {
      glm::vec2(-1.0, -1.0),
      glm::vec2(3.0, -1.0),
      glm::vec2(-1.0, 3.0),
  };

  GLuint fullscreen_mesh_vertices_vbo;
  glGenBuffers(1, &fullscreen_mesh_vertices_vbo);
  glBindBuffer(GL_ARRAY_BUFFER, fullscreen_mesh_vertices_vbo);
  glBufferData(GL_ARRAY_BUFFER, 3 * sizeof(glm::vec2),
               &fullscreen_mesh_vertices[0], GL_STATIC_DRAW);
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, static_cast<void*>(0));
  glBindBuffer(GL_ARRAY_BUFFER, 0);

  glBindVertexArray(0);

  return fullscreen_mesh_vertices_vbo;
}

const GLuint SceneRenderer::CreateFullscreenMeshUvsVbo(
    const GLuint fullscreen_mesh_vao) {
  glBindVertexArray(fullscreen_mesh_vao);

  const std::array<glm::vec2, 3> fullscreen_mesh_uvs = {
      glm::vec2(0.0, 0.0), glm::vec2(2.0, 0.0), glm::vec2(0.0, 2.0)};

  GLuint fullscreen_mesh_uvs_vbo;
  glGenBuffers(1, &fullscreen_mesh_uvs_vbo);
  glBindBuffer(GL_ARRAY_BUFFER, fullscreen_mesh_uvs_vbo);
  glBufferData(GL_ARRAY_BUFFER, 3 * sizeof(glm::vec2), &fullscreen_mesh_uvs[0],
               GL_STATIC_DRAW);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, static_cast<void*>(0));
  glBindBuffer(GL_ARRAY_BUFFER, 0);

  glBindVertexArray(0);

  return fullscreen_mesh_uvs_vbo;
}

コンストラクタを次のように変更します。

scene_renderer.cpp
SceneRenderer::SceneRenderer(const GLuint width, const GLuint height)
    : width_(width),
      height_(height),
      fullscreen_mesh_vao_(CreateFullscreenMeshVao()),      fullscreen_mesh_vertices_vbo_(          CreateFullscreenMeshVerticesVbo(fullscreen_mesh_vao_)),      fullscreen_mesh_uvs_vbo_(          CreateFullscreenMeshUvsVbo(fullscreen_mesh_vao_)),      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_)),
      hdr_color_buffer_(CreateHdrColorBuffer(width, height)),      hdr_depth_buffer_(CreateHdrDepthBuffer(width, height)),      hdr_fbo_(CreateHdrFbo(hdr_color_buffer_, hdr_depth_buffer_)),      geometry_pass_(gbuffer_fbo_),
      directional_light_pass_(hdr_fbo_, gbuffer0_, gbuffer1_, gbuffer2_,                              fullscreen_mesh_vao_, width, height) {}

SceneRenderer::Renderメンバ関数を次のように変更します。

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

  // HDRカラーバッファにDepthとStencilの転写
  glBindFramebuffer(GL_READ_FRAMEBUFFER, gbuffer_fbo_);
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, hdr_fbo_);
  glBlitFramebuffer(0, 0, width_, height_, 0, 0, width_, height_,
                    GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, GL_NEAREST);

  directional_light_pass_.Render(scene);

  glBindFramebuffer(GL_READ_FRAMEBUFFER, hdr_fbo_);
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
  glReadBuffer(GL_COLOR_ATTACHMENT0);
  glBlitFramebuffer(0, 0, width_, height_, 0, 0, width_, height_,
                    GL_COLOR_BUFFER_BIT, GL_NEAREST);
}

GeometryPassで書き出したバッファからデプスとステンシルを転写しているのがポイントです。 ステンシルはDirectionalLightの描画範囲を決めます。 デプスは次回以降のポイントライトやスポットライトなどの描画範囲を決めるのに使います。

shaderの変更点

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

shader/DirectionalLightPass.vert
#version 460

layout (location = 0) in vec2 position;
layout (location = 1) in vec2 uv;

out vec2 vUv;

void main()
{
  vUv = uv;
  gl_Position = vec4(position, 0.0, 1.0);
}

全画面を覆う三角形のuvを渡しています。

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

shader/DirectionalLightPass.frag
#version 460

in vec2 vUv;

layout (location = 0) out vec3 outRadiance;

layout (binding = 0) uniform sampler2D GBuffer0;
layout (binding = 1) uniform sampler2D GBuffer1;
layout (binding = 2) uniform sampler2D GBuffer2;

uniform vec3 lightDirection;
uniform float lightIntensity; // lx
uniform vec3 lightColor;

uniform vec3 worldCameraPos;
uniform mat4 ViewProjectionI;
uniform vec2 ProjectionParams; // x: near, y: far


const float PI = 3.14159265358979323846;


// world pos from depth texture ##############################################
float DecodeDepth(float d)
{
  return -d * (ProjectionParams.y - ProjectionParams.x) - ProjectionParams.x;
}

vec3 worldPosFromDepth(float d)
{
  float depth = DecodeDepth(d);
  float m22 = -(ProjectionParams.y + ProjectionParams.x) / (ProjectionParams.y - ProjectionParams.x);
  float m23 = -2.0 * ProjectionParams.y * ProjectionParams.x / (ProjectionParams.y - ProjectionParams.x);
  float z = depth * m22 + m23;
  float w = -depth;
  vec4 projectedPos = vec4(vUv.x * 2.0 - 1.0, vUv.y * 2.0 - 1.0, z / w, 1.0);
  vec4 worldPos = ViewProjectionI * projectedPos;
  return worldPos.xyz / worldPos.w;
}
// ###########################################################################


// BRDF ######################################################################
vec3 NormalizedLambert(vec3 diffuseColor) {
  return diffuseColor / PI;
}

vec3 F_Schlick(vec3 F0, vec3 H, vec3 V) {
  return (F0 + (1.0 - F0) * pow(1.0 - max(dot(V, H), 0), 5.0));
}

float D_GGX(float a, float NoH) {
  float a2 = a * a;
  float NoH2 = NoH * NoH;
  float d = NoH2 * (a2 - 1.0) + 1.0;
  return a2 / (PI * d * d);
}

float G_SchlickGGX(float a, float NoV) {
  float EPSILON = 0.001;
  float k = a * a / 2 + EPSILON;
  float up = NoV;
  float down = NoV * (1 - k) + k;
  return up / down;
}

float G_Smith(float a, float NoV, float NoL) {
  float ggx0 = G_SchlickGGX(1, NoV);
  float ggx1 = G_SchlickGGX(1, NoL);
  return ggx0 * ggx1;
}

vec3 BRDF(vec3 albedo, float metallic, float roughness, vec3 N, vec3 V, vec3 L, vec3 H) {
  float EPSILON = 0.001;
  vec3 F0 = mix(vec3(0.04), albedo, metallic);
  float NoH = max(dot(N, H), 0);
  float NoV = max(dot(N, V), 0);
  float NoL = max(dot(N, L), 0);
  float a = roughness * roughness;

  // specular
  vec3 F = F_Schlick(F0, H, V);
  float D = D_GGX(a, NoH);
  float G = G_Smith(a, NoV, NoL);
  vec3 up = F * D * G;
  float down = max(4.0 * NoV * NoL, EPSILON);
  vec3 specular = up / down;

  // diffuse
  vec3 diffuse = NormalizedLambert(albedo);

  vec3 kD = vec3(1.0) - F;
  kD *= 1.0 - metallic;
  return kD * diffuse + specular;
}
// ###########################################################################


// ACES ######################################################################
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;
}
// ###########################################################################


void main()
{
  vec4 gbuffer0 = texture(GBuffer0, vUv);
  vec4 gbuffer1 = texture(GBuffer1, vUv);
  vec4 gbuffer2 = texture(GBuffer2, vUv);

  vec3 albedo = gbuffer0.rgb;
  float metallic = gbuffer0.a;
  vec3 emissive = gbuffer1.rgb;
  float depth = gbuffer1.a;
  vec3 normal = gbuffer2.rgb * 2.0 - 1.0;
  float roughness = gbuffer2.a;

  vec3 worldPos = worldPosFromDepth(depth);

  vec3 V = normalize(worldCameraPos - worldPos);
  vec3 N = normalize(normal);
  vec3 L = normalize(-lightDirection);
  vec3 H = normalize(L + V);

  vec3 irradiance = lightIntensity * sRGBToACES(lightColor) * dot(N, L);
  vec3 radianceDirectionalLight = BRDF(albedo, metallic, roughness, N, V, L, H) * irradiance;

  outRadiance = emissive + radianceDirectionalLight;
}

main関数の中身を見ていくと、最初にgbufferからパラメータを取り出しています。

void main()
{
  vec4 gbuffer0 = texture(GBuffer0, vUv);
  vec4 gbuffer1 = texture(GBuffer1, vUv);
  vec4 gbuffer2 = texture(GBuffer2, vUv);

  vec3 albedo = gbuffer0.rgb;
  float metallic = gbuffer0.a;
  vec3 emissive = gbuffer1.rgb;
  float depth = gbuffer1.a;
  vec3 normal = gbuffer2.rgb * 2.0 - 1.0;
  float roughness = gbuffer2.a;

次にdepthからフラグメントのワールド位置を計算しています。

  vec3 worldPos = worldPosFromDepth(depth);
float DecodeDepth(float d)
{
  return -d * (ProjectionParams.y - ProjectionParams.x) - ProjectionParams.x;
}

vec3 worldPosFromDepth(float d)
{
  float depth = DecodeDepth(d);
  float m22 = -(ProjectionParams.y + ProjectionParams.x) / (ProjectionParams.y - ProjectionParams.x);
  float m23 = -2.0 * ProjectionParams.y * ProjectionParams.x / (ProjectionParams.y - ProjectionParams.x);
  float z = depth * m22 + m23;
  float w = -depth;
  vec4 projectedPos = vec4(vUv.x * 2.0 - 1.0, vUv.y * 2.0 - 1.0, z / w, 1.0);
  vec4 worldPos = ViewProjectionI * projectedPos;
  return worldPos.xyz / worldPos.w;
}

最初にGeometryPassでデコードした線形デプスを復元しています。

次にProjectionParamsをもとにprojectionがかかったあとの位置座標を求めています。 m22、m23はそれぞれProjection行列の要素に対応しています。 フラグメントのz座標は、線形のdepthがview座標系でのz座標になるので、線形のdepthとm22、m23から求まります。 フラグメントのxy座標はUVから求まります。

こうして求まったprojection行列がかかったあとのクリップスペースの位置にViewProjection行列の逆行列をかけてやることでワールド座標系を復元しています。

その後は幾何情報を求めて、照度を求めてBRDFと掛け合わせています。 BRDFは前々回に利用したものと同じで、正規化ランバートとクックトランスです。

  vec3 V = normalize(worldCameraPos - worldPos);
  vec3 N = normalize(normal);
  vec3 L = normalize(-lightDirection);
  vec3 H = normalize(L + V);

  vec3 irradiance = lightIntensity * sRGBToACES(lightColor) * dot(N, L);
  vec3 radianceDirectionalLight = BRDF(albedo, metallic, roughness, N, V, L, H) * irradiance;

  outRadiance = emissive + radianceDirectionalLight;

実行結果

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

2020 04 06 19 21 38

exposureとトーンマッピング、ガンマ補正がかかっていないので正しいライティングの結果が表示されませんが、問題なく動いていそうです。

プログラム全文

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

GitHub: MatchaChoco010/OpenGL-PBR-Map at v11