OpenGLでDeferredシェーディングを実装する(物理ベースカメラ)

はじめに

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

この記事ではPBRの古典的Deferredシェーディングに物理ベースカメラを加えてAuto Exposure処理を実装してみようと思います。

物理ベースカメラ

今回はこちらの記事に紹介されているFrostbiteで実装されたphysically based cameraを実装していきます。 記事のとおりに実装していくだけなので、詳しいことを知りたい人は記事を参照してください。

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

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

LogAveragePassクラスの作成

画面の明るさの対数平均を求めるLogAveragePassのクラスを作成します。

log_average_pass.h
#ifndef OPENGL_PBR_MAP_LOG_AVERAGE_PASS_H_
#define OPENGL_PBR_MAP_LOG_AVERAGE_PASS_H_

#include <algorithm>

#include "create_program.h"

namespace game {

/**
 * @brief LogAverageパスを表現するクラス
 *
 * HDRカラーバッファの輝度LのLogAverageを計算します。
 */
class LogAveragePass {
 public:
  /**
   * @brief このパスをレンダリングする
   *
   * HDRバッファの輝度のLogを書き出します。
   */
  void Render();

  /**
   * @brief LLogAverageの値を取得する
   * @return LLogAverageの値
   *
   * Renderを呼んだあとに呼び出します。
   */
  const GLfloat GetLLogAverage() const;

  /**
   * @brief コンストラクタ
   * @param hdr_color_buffer HDRカラーバッファのテクスチャID
   * @param fullscreen_mesh_vao 画面を覆うメッシュのVAO
   * @param width ウィンドウの幅
   * @param height ウィンドウの高さ
   */
  LogAveragePass(const GLuint hdr_color_buffer,
                 const GLuint fullscreen_mesh_vao_, const GLuint width,
                 const GLuint height);

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

 private:
  const GLuint width_;
  const GLuint height_;

  const GLuint hdr_color_buffer_;
  const GLuint fullscreen_mesh_vao_;

  const GLuint log_average_buffer_;
  const GLuint log_average_fbo_;

  const GLuint shader_program_;

  GLfloat l_average_;

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

  /**
   * @brief LogAverageのFBOを生成する
   * @param log_average_buffer LogAverageバッファテクスチャのID
   * @return 生成したFBOのID
   */
  static const GLuint CreateLogAverageFbo(const GLuint log_average_buffer);
};

}  // namespace game

#endif  // OPENGL_PBR_MAP_LOG_AVERAGE_PASS_H_
log_average_pass.cpp
#include "log_average_pass.h"

namespace game {

void LogAveragePass::Render() {
  glUseProgram(shader_program_);
  glBindFramebuffer(GL_FRAMEBUFFER, log_average_fbo_);

  glDisable(GL_STENCIL_TEST);

  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, hdr_color_buffer_);

  glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
  glClear(GL_COLOR_BUFFER_BIT);

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

  glBindTexture(GL_TEXTURE_2D, log_average_buffer_);
  glGenerateMipmap(GL_TEXTURE_2D);

  const int level = static_cast<int>(std::log2(std::max(width_, height_)));
  GLfloat pixel[3];
  glGetTexImage(GL_TEXTURE_2D, level, GL_RGB, GL_FLOAT, &pixel);
  l_average_ = std::expf(pixel[0]);

  glBindTexture(GL_TEXTURE_2D, 0);
}

const GLfloat LogAveragePass::GetLLogAverage() const { return l_average_; }

LogAveragePass::LogAveragePass(const GLuint hdr_color_buffer,
                               const GLuint fullscreen_mesh_vao,
                               const GLuint width, const GLuint height)
    : width_(width),
      height_(height),

      hdr_color_buffer_(hdr_color_buffer),
      fullscreen_mesh_vao_(fullscreen_mesh_vao),

      log_average_buffer_(CreateLogAverageBuffer(width, height)),
      log_average_fbo_(CreateLogAverageFbo(log_average_buffer_)),

      shader_program_(CreateProgram("shader/LogAveragePass.vert",
                                    "shader/LogAveragePass.frag")),
      l_average_(0) {}

LogAveragePass::~LogAveragePass() {
  glDeleteFramebuffers(1, &log_average_fbo_);
  glDeleteTextures(1, &log_average_buffer_);

  glDeleteProgram(shader_program_);
}

const GLuint LogAveragePass::CreateLogAverageBuffer(const GLuint width,
                                                    const GLuint height) {
  GLuint log_average_buffer;
  glGenTextures(1, &log_average_buffer);
  glBindTexture(GL_TEXTURE_2D, log_average_buffer);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT,
               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_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glBindTexture(GL_TEXTURE_2D, 0);
  return log_average_buffer;
}

const GLuint LogAveragePass::CreateLogAverageFbo(
    const GLuint log_average_buffer) {
  GLuint log_average_fbo;
  glGenFramebuffers(1, &log_average_fbo);
  glBindFramebuffer(GL_FRAMEBUFFER, log_average_fbo);
  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
                         log_average_buffer, 0);
  glBindFramebuffer(GL_FRAMEBUFFER, 0);
  return log_average_fbo;
}

}  // namespace game

輝度の対数平均を求めるためのフレームバッファを用意しています。 フレームバッファに対数期度をレンダリングした後にmipmap計算で平均を求めています。

LogAveragePass用のシェーダ作成

shader/LogAveragePass.vertshader/LogAveragePass.fragを作成します。

shader/LogAveragePass.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);
}
shader/LogAveragePass.frag
#version 460

in vec2 vUv;

layout (location = 0) out float outputColor;

uniform sampler2D inputTexture;


const float HALF_MAX = 65504.0;


float luminance(vec3 rgb)
{
  return 0.298912 * rgb.r + 0.586611 * rgb.g + 0.114478 * rgb.b;
}

void main()
{
  vec3 inputColor = texture(inputTexture, vUv).rgb;
  inputColor = clamp(inputColor, 0, HALF_MAX);

  float l = luminance(inputColor);
  const float EPSILON = 0.01;
  outputColor = log(l + EPSILON);
}

入力画像の輝度をとっているだけです。

PhysicallyBasedCameraクラスの作成

物理ベースカメラを表現するクラスを実装します。

physically_based_camera.h
#ifndef OPENGL_PBR_MAP_PHYSICALLY_BASED_CAMERA_H_
#define OPENGL_PBR_MAP_PHYSICALLY_BASED_CAMERA_H_

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

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

namespace game {

/**
 * @brief Physically Based Cameraのクラス
 *
 * 物理ベースの露出補正を行うカメラのクラスです。
 * Frostbiteの2014年のSIGGRAPHの資料をベースにした次の記事を参考にしました。
 * https://placeholderart.wordpress.com/2014/11/16/implementing-a-physically-based-camera-understanding-exposure/
 * https://placeholderart.wordpress.com/2014/11/21/implementing-a-physically-based-camera-manual-exposure/
 * https://placeholderart.wordpress.com/2014/12/15/implementing-a-physically-based-camera-automatic-exposure/
 */
class PhysicallyBasedCamera final {
 public:
  /**
   * @brief カメラの露出を更新する
   * @param l_new 画面輝度の対数平均値
   * @param delta_time 前フレームからの時間(秒)
   */
  void Update(const GLfloat l_new, const double delta_time);

  /**
   * @brief ISOの値を取得する
   * @return ISOの値
   */
  const GLfloat GetIso() const;

  /**
   * @brief 絞りの値を取得する
   * @return 絞りの値
   */
  const GLfloat GetAperture() const;

  /**
   * @brief シャッタースピードを取得する
   * @return シャッタースピード(秒)
   */
  const GLfloat GetShutterSpeed() const;

  /**
   * @brief EVcompの値を取得する
   * @return EVcompの値
   */
  const GLfloat GetEvComop() const;

  /**
   * @brief EVcompの値を設定する
   * @param ev_comp 新しいEVcompの値
   */
  void SetEvComp(const GLfloat ev_comp);

  /**
   * @brief 露出を取得する
   * @return 露出の値
   */
  const GLfloat GetExposure() const;

  /**
   * @brief コンストラクタ
   * @param l_avg 画面平均輝度の初期値
   * @param ev_comp EVcompの初期値
   */
  PhysicallyBasedCamera(const GLfloat l_avg, const GLfloat ev_comp);

 private:
  static constexpr GLfloat kMinIso = 100.0f;
  static constexpr GLfloat kMaxIso = 6400.0f;
  static constexpr GLfloat kMinAperture = 1.8f;
  static constexpr GLfloat kMaxAperture = 22.0f;
  static constexpr GLfloat kMinShutterSpeed = 1.0f / 4000.0f;
  static constexpr GLfloat kMaxShutterSpeed = 1.0f / 30.0f;

  GLfloat l_avg_;
  GLfloat iso_;
  GLfloat aperture_;
  GLfloat shutter_speed_;
  GLfloat ev_comp_;
  GLfloat exposure_;

  /**
   * @brief TargetEVを計算する
   * @return TargetEVの値
   */
  const GLfloat ComputeTargetEv(const GLfloat average_luminance);

  /**
   * @brief EVに合わせたISOの値を計算する
   * @param aperture 絞りの値
   * @param shutter_speed シャッタースピード(秒)
   * @param ev ターゲットとなるEVの値
   * @return 計算されたISOの値
   */
  const GLfloat ComputeIso(const GLfloat aperture, const GLfloat shutter_speed,
                           const GLfloat ev);

  /**
   * @brief EVの値を計算する
   * @param aperture 絞りの値
   * @param shutter_speed シャッタースピード(秒)
   * @param iso ISOの値
   * @return 計算されたEVの値
   */
  const GLfloat ComputeEv(const GLfloat aperture, const GLfloat shutter_speed,
                          const GLfloat iso);
};

}  // namespace game

#endif  // OPENGL_PBR_MAP_PHYSICALLY_BASED_CAMERA_H_
physically_based_camera.cpp
#include "physically_based_camera.h"

namespace game {

void PhysicallyBasedCamera::Update(const GLfloat l_new,
                                   const double delta_time) {
  l_avg_ = l_avg_ + (l_new - l_avg_) * (1 - std::expf(-1 * delta_time * 1.0));

  GLfloat target_ev = ComputeTargetEv(l_avg_);
  target_ev = target_ev - ev_comp_;

  aperture_ = 4.0f;
  shutter_speed_ = 1.0f / 100.0f;

  iso_ = std::clamp(ComputeIso(aperture_, shutter_speed_, target_ev), kMinIso,
                    kMaxIso);

  GLfloat evDiff = target_ev - ComputeEv(aperture_, shutter_speed_, iso_);
  aperture_ = std::clamp(aperture_ * std::powf(std::sqrt(2.0f), evDiff * 0.5f),
                         kMinAperture, kMaxAperture);

  evDiff = target_ev - ComputeEv(aperture_, shutter_speed_, iso_);
  shutter_speed_ = std::clamp(shutter_speed_ * std::powf(2.0f, -evDiff),
                              kMinShutterSpeed, kMaxShutterSpeed);

  const GLfloat l_max =
      (7800.0f / 65.0f) * (aperture_ * aperture_) / (iso_ * shutter_speed_);
  exposure_ = 1.0f / l_max;
}

const GLfloat PhysicallyBasedCamera::GetIso() const { return iso_; }

const GLfloat PhysicallyBasedCamera::GetAperture() const { return aperture_; }

const GLfloat PhysicallyBasedCamera::GetShutterSpeed() const {
  return shutter_speed_;
}

const GLfloat PhysicallyBasedCamera::GetEvComop() const { return ev_comp_; }

void PhysicallyBasedCamera::SetEvComp(const GLfloat ev_comp) {
  ev_comp_ = ev_comp;
}

const GLfloat PhysicallyBasedCamera::GetExposure() const { return exposure_; }

PhysicallyBasedCamera::PhysicallyBasedCamera(const GLfloat l_avg,
                                             const GLfloat ev_comp)
    : l_avg_(l_avg), ev_comp_(ev_comp) {
  Update(l_avg, 0.0);
}

const GLfloat PhysicallyBasedCamera::ComputeTargetEv(
    const GLfloat average_luminance) {
  return std::log2(average_luminance * 100.0f / 12.5f);
}

const GLfloat PhysicallyBasedCamera::ComputeIso(const GLfloat aperture,
                                                const GLfloat shutter_speed,
                                                const GLfloat ev) {
  return (aperture * aperture * 100.0f) / (shutter_speed * std::powf(2.0f, ev));
}

const GLfloat PhysicallyBasedCamera::ComputeEv(const GLfloat aperture,
                                               const GLfloat shutter_speed,
                                               const GLfloat iso) {
  return std::log2((aperture * aperture * 100.0f) / (shutter_speed * iso));
}

}  // namespace game

SceneRendererクラスの変更点

LogAveragePassとPhysicallyBasedCameraを追加します。

scene_renderer.h
#include "directional_light_pass.h"
#include "exposure_pass.h"
#include "geometry_pass.h"
#include "log_average_pass.h"#include "physically_based_camera.h"#include "point_light_pass.h"
#include "scene.h"
#include "sky_pass.h"
#include "spot_light_pass.h"
#include "tonemapping_pass.h"
scene_renderer.h
  GeometryPass geometry_pass_;
  SkyPass sky_pass_;
  DirectionalLightPass directional_light_pass_;
  PointLightPass point_light_pass_;
  SpotLightPass spot_light_pass_;
  LogAveragePass log_average_pass_;  ExposurePass exposure_pass_;
  TonemappingPass tonemapping_pass_;

  PhysicallyBasedCamera physically_based_camera_;

コンストラクタを変更します。

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_)),

      sphere_vao_(CreateSphereMeshVao()),
      sphere_vertices_vbo_(CreateSphereMeshVbo(sphere_vao_)),
      sphere_indices_ibo_(CreateSphereMeshIbo(sphere_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_)),

      exposured_color_buffer_(CreateExposuredColorBuffer(width, height)),
      exposured_fbo_(CreateExposuredFbo(exposured_color_buffer_)),

      geometry_pass_(gbuffer_fbo_),
      sky_pass_(hdr_fbo_, fullscreen_mesh_vao_, width_, height_),
      directional_light_pass_(hdr_fbo_, gbuffer0_, gbuffer1_, gbuffer2_,
                              fullscreen_mesh_vao_, width, height),
      point_light_pass_(hdr_fbo_, gbuffer0_, gbuffer1_, gbuffer2_, sphere_vao_,
                        width, height),
      spot_light_pass_(hdr_fbo_, gbuffer0_, gbuffer1_, gbuffer2_, sphere_vao_,
                       width, height),
      log_average_pass_(hdr_color_buffer_, fullscreen_mesh_vao_, width, height),      exposure_pass_(hdr_color_buffer_, exposured_fbo_, fullscreen_mesh_vao_,
                     width, height),
      tonemapping_pass_(exposured_color_buffer_, fullscreen_mesh_vao_, width,
                        height),

      physically_based_camera_(0, 0) {}

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

  sky_pass_.Render(scene);

  directional_light_pass_.Render(scene);

  point_light_pass_.Render(scene);

  spot_light_pass_.Render(scene);

  log_average_pass_.Render();  const auto l_average = log_average_pass_.GetLLogAverage();
  physically_based_camera_.Update(l_average, delta_time);  const auto exposure = physically_based_camera_.GetExposure();
  exposure_pass_.SetExposure(exposure);  exposure_pass_.Render();

  tonemapping_pass_.Render();
}

SceneRendererにEV値をセットするメンバ関数を作成します。

scene_renderer.h
  /**
   * @brief EVcompの値を設定する
   * @param ev_comp 新しいEVcompの値
   */
  void SetEvComp(const GLfloat ev_comp);
scene_renderer.cpp
void SceneRenderer::SetEvComp(const GLfloat ev_comp) {
  physically_based_camera_.SetEvComp(ev_comp);
}

実行結果

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

2020 04 13 12 07 31

EVCompの値を変更してみます。 Application::Initで適当に次のように記述します。

application.cpp
  // SceneRendererの作成
  scene_renderer_ = std::make_unique<SceneRenderer>(width, height);
  scene_renderer_->SetEvComp(-3.0f);

すると次のようになります。

2020 04 13 12 06 53

シーンの明るさに合わせて順応していることがわかります。

プログラム全文

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

GitHub: MatchaChoco010/OpenGL-PBR-Map at v20