OpenGLでDeferredシェーディングを実装する(物理ベースカメラ)
はじめに
この記事はシリーズ記事です。目次はこちら。
この記事ではPBRの古典的Deferredシェーディングに物理ベースカメラを加えてAuto Exposure処理を実装してみようと思います。
物理ベースカメラ
今回はこちらの記事に紹介されているFrostbiteで実装されたphysically based cameraを実装していきます。 記事のとおりに実装していくだけなので、詳しいことを知りたい人は記事を参照してください。
- Implementing a Physically Based Camera: Understanding Exposure | Placeholder Art
- Implementing a Physically Based Camera: Understanding Exposure | Placeholder Art
- Implementing a Physically Based Camera: Understanding Exposure | Placeholder Art
前回のプログラムからの変更点
前回のプログラムからの変更点を次に示します。
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.vert
とshader/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);
}
実行結果
実行結果は次のとおりです。
EVCompの値を変更してみます。
Application::Init
で適当に次のように記述します。
application.cpp
// SceneRendererの作成
scene_renderer_ = std::make_unique<SceneRenderer>(width, height);
scene_renderer_->SetEvComp(-3.0f);
すると次のようになります。
シーンの明るさに合わせて順応していることがわかります。
プログラム全文
プログラム全文はGitHubにアップロードしてあります。