OpenGLでDeferredシェーディングを実装する(Specular IBL)
はじめに
この記事はシリーズ記事です。目次はこちら。
この記事ではPBRの古典的DeferredシェーディングにSpecularのIBLを追加しようと思います。
Specular IBL
今回実装するSpecular IBLは次の資料に従って実装します。
DiffuseのIBLと同じようにSpecular成分について式を立ててみます。
ここで、は次の式です。
より詳しく書くと次のとおりです。
は反射の位置です。はライトの方向ベクトル、はビューの方向ベクトル、はハーフベクトル、は法線ベクトルです。 あるの方向からの方向に向かって反射する光を、半球のすべての方向からのについて積分してに向かって反射する光というのを求めています。
ここでは次の式です。
は次のとおりです。
は先の資料では普通のSchlickの近似から微妙に変えたものを使っているようです。
このは、定数だったと異なり、Viewの方向とLightの方向にも依存していることがわかります。 事前計算結果をキューブマップに焼き込んでNormalの方向からサンプリングすればよかったDiffuseのIBLと同じようには行きません。
とりあえずインテグラルを数値計算のモンテカルロ積分に置き換えます。
分母のは確率密度関数(pdf)です。 モンテカルロ積分のインポータンスサンプリングというものになっています。 これについては次のページなどがわかりやすいです。
- Chapter 20. GPU-Based Importance Sampling | NVIDIA Developer
- Monte Carlo Methods in Practice (Variance Reduction Methods: a Quick Introduction to Importance Sampling)
スペキュラ反射は鏡面反射の方向ほど影響度が強いピークのある関数になっています。 こういう偏った関数をモンテカルロ積分で求める場合は、uniformな確率でランダムにサンプリングしても良いのですが、関数に合わせた形の確率でサンプリングしたほうが少ないサンプル数で精度が上がります。
さて、このモンテカルロによる近似式をさらに大胆に2つに分割するという近似を行います。
ここで、前半のライトの部分について事前計算をしてキューブマップに収めます。 というのはインポータンスサンプリングの確率によってサンプリング点を求めるわけですが、インポータンスサンプリングに使う確率はRoughnessに応じて反射のピークの形が変わるのでそれに合わせて変更しています。 そこで、いくつかのRoughness値について事前計算をする必要があります。 Roughnessが粗くなるにつれて反射はぼやけたものになるので、Roughnessが低い値では高周波成分が必要ですが粗くなると小さい画像に格納しても問題なくなります。 これはキューブマップのmipmapに入れるのに適していることになります。 今回はRoughnessが0、0.25、0.5、0.75、1.0の場合をmipmapに格納し、その間の値についてはmipmapのブレンドを使うことにします。
本来、マイクロファセットベースのGGXの分布は面に対する視点の方向によって変化するはずですが、ここではのような場合と仮定して近似的に求めてしまうことにします。 この近似によってグレージング角付近のスペキュラの引き伸ばされたような反射は再現できなくなります。
(画像はMoving Frostbite to PBRの記事より)
先の資料に書かれているこの部分のコードをGLSLにしたものはこのようになります。
vec3 PrefilterEnvMap( float Roughness, vec3 R )
{
vec3 N = R;
vec3 V = R;
vec3 PrefilteredColor = 0;
const uint NumSamples = 1024;
for( uint i = 0; i < NumSamples; i++ )
{
vec2 Xi = Hammersley( i, NumSamples );
vec3 H = ImportanceSampleGGX( Xi, Roughness, N );
vec3 L = 2 * dot( V, H ) * H - V;
float NoL = saturate( dot( N, L ) );
if( NoL > 0 )
{
PrefilteredColor += EnvMap.SampleLevel( EnvMapSampler , L, 0 ).rgb * NoL;
TotalWeight += NoL;
}
}
return PrefilteredColor / TotalWeight;
}
ここでHammersley
というのは乱数の代わりに使う低食い違い量列(Low Discrepancy Sequence, LDS)というものです。
このように乱数の代わりにすでに決まっている低食い違い量列をつかうのは準モンテカルロ法と呼ばれます。 準モンテカルロ法は低次元の積分においてモンテカルロ法より収束が早いという特徴があります。 そのためここでは乱数ではなく低食い違い量列を使っています。
先の2つに分割した近似の前半についてはキューブマップにmipmapで収めるということで説明しました。 つぎに後半の部分について説明します。
後半の方の式はについて整理すると次のようになります。
このようにについて整理してやると、との2つのinputからに対するスケール()とバイアス()の2つのoutputを事前計算してやれば良さそうです。 幸いなことにすべて[0, 1]区間なのでテクスチャに焼き込むことができます。 2つのinputである[0, 1]のとをテクスチャの縦と横に割り当てて、そこから求められるに対するスケールとバイアスとをテクスチャのRG成分に書き出してやって、2DのLook-up texture(LUT)として使います。
(図は先の資料より。というのはのことです)
Specular IBLの事前計算用プログラムの作成
IBL-Specularというプロジェクトを新しく用意します。
GLFWやGLEW、tinyexrなどをプロジェクトから使えるようにセットアップします。
main.cppに全部書いていくことにします。
まずはヘッダのインクルードと、exrの読み込み保存、ShaderProgramの作成やGLFWの初期化処理などです。
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <array>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#define TINYEXR_IMPLEMENTATION
#include <tinyexr.h>
#include <glm/ext.hpp>
#include <glm/glm.hpp>
GLFWwindow* window;
const GLuint LoadExr(const std::string& path) {
float* data;
GLuint texture_id = 0;
int width;
int height;
const char* err = nullptr;
int ret = LoadEXR(&data, &width, &height, path.c_str(), &err);
if (ret != TINYEXR_SUCCESS) {
std::cerr << "Can't load image: " << path << std::endl;
if (err) {
std::cerr << "ERR : " << err << std::endl;
FreeEXRErrorMessage(err);
}
} else {
glGenTextures(1, &texture_id);
glBindTexture(GL_TEXTURE_2D, texture_id);
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_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA,
GL_FLOAT, data);
glGenerateMipmap(GL_TEXTURE_2D);
free(data);
glBindTexture(GL_TEXTURE_2D, 0);
}
return texture_id;
}
bool SaveExr(std::unique_ptr<float[]> data, const std::string& path,
const int width, const int height) {
EXRHeader header;
InitEXRHeader(&header);
EXRImage image;
InitEXRImage(&image);
image.num_channels = 3;
std::vector<float> images[3];
images[0].resize(width * height);
images[1].resize(width * height);
images[2].resize(width * height);
// Split RGBARGBARGBA... into R, G and B layer
for (int i = 0; i < width * height; i++) {
images[0][i] = data[4 * i + 0];
images[1][i] = data[4 * i + 1];
images[2][i] = data[4 * i + 2];
}
float* image_ptr[3];
image_ptr[0] = &(images[2].at(0)); // B
image_ptr[1] = &(images[1].at(0)); // G
image_ptr[2] = &(images[0].at(0)); // R
image.images = (unsigned char**)image_ptr;
image.width = width;
image.height = height;
header.num_channels = 3;
header.channels =
(EXRChannelInfo*)malloc(sizeof(EXRChannelInfo) * header.num_channels);
// Must be (A)BGR order, since most of EXR viewers expect this channel order.
header.channels[0].name[0] = 'B';
header.channels[0].name[1] = '\0';
header.channels[1].name[0] = 'G';
header.channels[1].name[1] = '\0';
header.channels[2].name[0] = 'R';
header.channels[2].name[1] = '\0';
header.pixel_types = (int*)malloc(sizeof(int) * header.num_channels);
header.requested_pixel_types =
(int*)malloc(sizeof(int) * header.num_channels);
for (int i = 0; i < header.num_channels; i++) {
header.pixel_types[i] =
TINYEXR_PIXELTYPE_FLOAT; // pixel type of input image
header.requested_pixel_types[i] =
TINYEXR_PIXELTYPE_FLOAT; // pixel type of output image to be stored in
// .EXR
}
const char* err = nullptr;
int ret = SaveEXRImageToFile(&image, &header, path.c_str(), &err);
if (ret != TINYEXR_SUCCESS) {
std::cerr << "Save EXR err: " << err << std::endl;
FreeEXRErrorMessage(err); // free's buffer for an error message
return ret;
}
std::cout << "Saved exr file. " << path << std::endl;
free(header.channels);
free(header.pixel_types);
free(header.requested_pixel_types);
}
bool Init() {
glfwSetErrorCallback(
[](auto id, auto description) { std::cerr << description << std::endl; });
// GLFWの初期化
if (!glfwInit()) {
return false;
}
// OpenGL Version 4.6 Core Profileを選択する
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// リサイズ不可
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
// Window非表示
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
// ウィンドウの作成
window = glfwCreateWindow(64, 64, "", nullptr, nullptr);
if (window == nullptr) {
std::cerr << "Can't create GLFW window." << std::endl;
return false;
}
glfwMakeContextCurrent(window);
// GLEWの初期化
if (glewInit() != GLEW_OK) {
std::cerr << "Can't initialize GLEW." << std::endl;
return false;
}
return true;
}
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;
}
ここらへんは特に説明はいらないかなと。
LUTのベイクのプログラム
次にLUTをベイクするプログラムです。 main.cppに続けて書いていきます。
/**
* @brief LUTをベイクする
*/
void BakeLut(const std::string& output_path) {
// textureの作成
GLuint lut_texture;
glGenTextures(1, &lut_texture);
glBindTexture(GL_TEXTURE_2D, lut_texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 256, 256, 0, GL_RGBA, 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_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, 0);
// fboの作成
GLuint lut_fbo;
glGenFramebuffers(1, &lut_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, lut_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
lut_texture, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// fullscreen meshの作成
GLuint fullscreen_mesh_vao;
glGenVertexArrays(1, &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));
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));
// programの作成
GLuint shader_program =
CreateProgram("IBLSpecularShader/Lut.vert", "IBLSpecularShader/Lut.frag");
// Bake
glUseProgram(shader_program);
glBindFramebuffer(GL_FRAMEBUFFER, lut_fbo);
glViewport(0, 0, 256, 256);
glBindVertexArray(fullscreen_mesh_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::unique_ptr<float[]> data = std::make_unique<float[]>(4 * 256 * 256);
glReadPixels(0, 0, 256, 256, GL_RGBA, GL_FLOAT, data.get());
SaveExr(std::move(data), output_path, 256, 256);
}
fullscreenなメッシュを作成して描画した結果を保存しています。
IBLSpecularShader/Lut.vert
とIBLSpecularShader/Lut.frag
は次のとおりです。
#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);
}
#version 460
in vec2 vUv;
layout (location = 0) out vec2 outputColor;
const float PI = 3.14159265358979323846;
vec3 ImportanceSampleGGX(vec2 Xi, float Roughness, vec3 N) {
float a = Roughness * Roughness;
float Phi = 2.0 * PI * Xi.x;
float CosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a * a - 1.0) * Xi.y));
float SinTheta = sqrt(1.0 - CosTheta * CosTheta);
vec3 H;
H.x = SinTheta * cos(Phi);
H.y = CosTheta;
H.z = SinTheta * sin(Phi);
vec3 UpVector = abs(N.y) < 0.999 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
vec3 TangentX = normalize(cross(UpVector, N));
vec3 TangentZ = cross(N, TangentX);
return TangentX * H.x + N * H.y + TangentZ * H.z;
}
float radicalInverse_VdC(uint bits) {
bits = (bits << 16u) | (bits >> 16u);
bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}
// http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html
vec2 Hammersley(uint i, uint N) {
return vec2(float(i) / float(N), radicalInverse_VdC(i));
}
float G1(float Roughness, float NoV) {
float k = Roughness * Roughness / 2.0;
return NoV / (NoV * (1 - k) + k);
}
float G_Smith(float Roughness, float NoV, float NoL) {
return G1(Roughness, NoV) * G1(Roughness, NoL);
}
void main()
{
float NoV = vUv.x;
float Roughness = vUv.y;
vec3 V;
V.x = sqrt(1.0 - NoV * NoV);
V.y = NoV;
V.z = 0.0;
vec3 N = vec3(0.0, 1.0, 0.0);
float A = 0.0;
float B = 0.0;
const uint NumSamples = 1024u;
for (uint i = 0; i < NumSamples; i++) {
vec2 Xi = Hammersley(i, NumSamples);
vec3 H = ImportanceSampleGGX(Xi, Roughness, N);
vec3 L = normalize(2.0 * dot(V, H) * H - V);
float NoL = max(L.y, 0);
float NoH = max(H.y, 0);
float VoH = max(dot(V, H), 0);
if (NoL > 0) {
float G = G_Smith(Roughness, NoV, NoL);
float G_Vis = G * VoH / (NoH * NoV);
float Fc = pow(1.0 - VoH, 5.0);
A += (1.0 - Fc) * G_Vis;
B += Fc * G_Vis;
}
}
A /= float(NumSamples);
B /= float(NumSamples);
outputColor = vec2(A, B);
}
上で説明した数式通りにプログラムを書いています。 とをUVから決めています。
Prefiletered Mapを作成するプログラム
次にprefiltered mapを計算するプログラムです。 これもmain.cppに追記していきます。
/**
* @brief キューブマップのPrefiltered Mapを生成する
*/
void BakePrefilteredMap(const std::string& input_path,
const float sky_intensity,
const std::string& output_path) {
// skyテクスチャの読み込み
const auto sky_exr = LoadExr(input_path);
// 各種roughness用のtextureとfboの作成
std::array<GLuint, 5> textures;
std::array<GLuint, 5> fbos;
for (int i = 0; i < 5; ++i) {
glGenTextures(1, &textures[i]);
glBindTexture(GL_TEXTURE_2D, textures[i]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 256 * std::pow(0.5, i),
256 * std::pow(0.5, i), 0, GL_RGBA, 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_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glGenFramebuffers(1, &fbos[i]);
glBindFramebuffer(GL_FRAMEBUFFER, fbos[i]);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
textures[i], 0);
}
// fullscreen meshの作成
GLuint fullscreen_mesh_vao;
glGenVertexArrays(1, &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));
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));
// programの作成とuniformのlocationの取得
const auto program = CreateProgram("IBLSpecularShader/shader.vert",
"IBLSpecularShader/shader.frag");
const auto rotation_loc = glGetUniformLocation(program, "Rotation");
const auto sky_intensity_loc = glGetUniformLocation(program, "skyIntensity");
const auto roughness_loc = glGetUniformLocation(program, "Roughness");
// カメラのrotation
const std::array<glm::mat4, 6> rotations = {
glm::inverse(glm::lookAt(glm::vec3(0), glm::vec3(1.0f, 0.0f, 0.0f),
glm::vec3(0.0f, -1.0f, 0.0f))),
glm::inverse(glm::lookAt(glm::vec3(0), glm::vec3(-1.0f, 0.0f, 0.0f),
glm::vec3(0.0f, -1.0f, 0.0f))),
glm::inverse(glm::lookAt(glm::vec3(0), glm::vec3(0.0f, 1.0f, 0.0f),
glm::vec3(0.0f, 0.0f, 1.0f))),
glm::inverse(glm::lookAt(glm::vec3(0), glm::vec3(0.0f, -1.0f, 0.0f),
glm::vec3(0.0f, 0.0f, -1.0f))),
glm::inverse(glm::lookAt(glm::vec3(0), glm::vec3(0.0f, 0.0f, 1.0f),
glm::vec3(0.0f, -1.0f, 0.0f))),
glm::inverse(glm::lookAt(glm::vec3(0), glm::vec3(0.0f, 0.0f, -1.0f),
glm::vec3(0.0f, -1.0f, 0.0f)))};
// 出力ファイル名
const std::array<std::string, 5> output_dirs = {
output_path + "/roughness-0", output_path + "/roughness-0-25",
output_path + "/roughness-0-5", output_path + "/roughness-0-75",
output_path + "/roughness-1",
};
const std::array<std::string, 6> output_filename = {
"pos-x.exr", "neg-x.exr", "pos-y.exr",
"neg-y.exr", "pos-z.exr", "neg-z.exr",
};
// bake
glUseProgram(program);
glUniform1f(sky_intensity_loc, sky_intensity);
for (int i = 0; i < 5; ++i) {
std::filesystem::create_directories(output_dirs[i]);
glBindFramebuffer(GL_FRAMEBUFFER, fbos[i]);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, sky_exr);
glUniform1f(roughness_loc, 0.25 * i);
for (int j = 0; j < 6; ++j) {
glUniformMatrix4fv(rotation_loc, 1, GL_FALSE, &rotations[j][0][0]);
glViewport(0, 0, 256 * std::pow(0.5, i), 256 * std::pow(0.5, i));
glBindVertexArray(fullscreen_mesh_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::unique_ptr<float[]> data = std::make_unique<float[]>(
4 * 256 * std::pow(0.5, i) * 256 * std::pow(0.5, i));
glReadPixels(0, 0, 256 * std::pow(0.5, i), 256 * std::pow(0.5, i),
GL_RGBA, GL_FLOAT, data.get());
SaveExr(std::move(data), output_dirs[i] + "/" + output_filename[j],
256 * std::pow(0.5, i), 256 * std::pow(0.5, i));
}
}
}
roughnessを0、0.25、0.5、0.75、1の5種類、キューブマップの6面について描画し保存しています。
IBLSpecularShader/shader.vert
とIBLSpecularShader/shader.frag
は次のとおりです。
#version 460
layout (location = 0) in vec2 position;
uniform mat4 Rotation;
out vec3 direction;
void main()
{
direction = (Rotation * vec4(position.x, position.y, -1.0, 0.0)).xyz;
gl_Position = vec4(position, 0.0, 1.0);
}
#version 460
in vec3 direction;
layout (location = 0) out vec4 outputColor;
layout (binding = 0) uniform sampler2D SkyImage;
uniform float skyIntensity;
uniform float Roughness;
const float PI = 3.14159265358979323846;
// 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;
}
// ###########################################################################
// Sample Color ##############################################################
vec3 SampleColor(vec3 dir) {
dir = normalize(dir);
vec2 texcoord = vec2(
atan(dir.z, dir.x) / (2 * PI) + 0.5,
atan(dir.y, length(dir.xz)) / PI + 0.5
);
// flip Y
texcoord.y = 1 - texcoord.y;
return sRGBToACES(texture(SkyImage, texcoord).rgb) * skyIntensity;
}
// ###########################################################################
// Importance Sampling GGX ###################################################
vec3 ImportanceSampleGGX(vec2 Xi, float Roughness, vec3 N) {
float a = Roughness * Roughness;
float Phi = 2.0 * PI * Xi.x;
float CosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a * a - 1.0) * Xi.y));
float SinTheta = sqrt(1.0 - CosTheta * CosTheta);
vec3 H;
H.x = SinTheta * cos(Phi);
H.y = CosTheta;
H.z = SinTheta * sin(Phi);
vec3 UpVector = abs(N.y) < 0.999 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
vec3 TangentX = normalize(cross(UpVector, N));
vec3 TangentZ = cross(N, TangentX);
return TangentX * H.x + N * H.y + TangentZ * H.z;
}
// ###########################################################################
// Hammersley ################################################################
float radicalInverse_VdC(uint bits) {
bits = (bits << 16u) | (bits >> 16u);
bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}
// http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html
vec2 Hammersley(uint i, uint N) {
return vec2(float(i) / float(N), radicalInverse_VdC(i));
}
// ###########################################################################
void main() {
vec3 dir = normalize(direction);
vec3 N = dir;
vec3 V = dir;
vec3 PrefilteredColor = vec3(0.0);
float TotalWeight = 0.0;
const uint NumSamples = 1024u;
for (uint i = 0; i < NumSamples; i++) {
vec2 Xi = Hammersley(i, NumSamples);
vec3 H = ImportanceSampleGGX(Xi, Roughness, N);
vec3 L = normalize(2.0 * dot(V, H) * H - V);
float NoL = max(dot(N, L), 0.0);
if (NoL > 0.0) {
PrefilteredColor += SampleColor(L) * NoL;
TotalWeight += NoL;
}
}
outputColor = vec4(PrefilteredColor / TotalWeight, 1.0);
}
資料のコード片とほとんど同じですね。
最後にmain関数からそれぞれの関数を呼び出すようにします。
int main(int argc, char* argv[]) {
if (argc <= 3) {
std::cerr << "src path, dst path and sky intensity is required." << std::endl;
return -1;
}
Init();
BakeLut(std::string(argv[3]) + "/Lut.exr");
BakePrefilteredMap(std::string(argv[1]), std::stof(std::string(argv[2])),
std::string(argv[3]));
}
Blender Addonの変更点
IBL-SpecularをリリースビルドしてIBL-Specular.exeを手に入れます。
__init__.py
やscene_exporter.py
と同じディレクトリにIBL-Specular.exe
とIBLSpecularShader/
を配置します。
scene_exporter.py
を次のように変更します。
...
# Global Diffuse IBL
os.makedirs(os.path.join(self.filepath, "GlobalIBL", "Diffuse"))
srcpath = os.path.join(self.filepath, "Sky", "sky.exr")
dstpath = os.path.join(self.filepath, "GlobalIBL", "Diffuse")
subprocess.call(["IBL-Diffuse.exe", srcpath,
str(sky_intensity), dstpath])
# Global Specular IBL os.makedirs(os.path.join(self.filepath, "GlobalIBL", "Specular")) srcpath = os.path.join(self.filepath, "Sky", "sky.exr") dstpath = os.path.join(self.filepath, "GlobalIBL", "Specular") subprocess.call(["IBL-Specular.exe", srcpath, str(sky_intensity), dstpath])
scene_text = "# Scene file\n"
...
アドオンをインストールし直して出力するとSpecular IBL用のテクスチャが出力されます。
前回のプログラムからの変更点
前回のプログラムからの変更点を次に示します。
SpecularIblExrCubemapTextureクラスの作成
mipmapに各Roughnessのキューブマップテクスチャを読み込むクラスを作成します。
#ifndef OPENGL_PBR_MAP_SPECULAR_IBL_EXR_CUBEMAP_TEXTURE_H_
#define OPENGL_PBR_MAP_SPECULAR_IBL_EXR_CUBEMAP_TEXTURE_H_
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <array>
#include <iostream>
#include <string>
namespace game {
/**
* @brief SpecularIBL用のCubemapテクスチャ
*
* OpenEXR形式のテクスチャを読み込みOpenGLにアップロードするクラスです。
* テクスチャの多重開放を避けるためコピー禁止です。
* ムーブは可能です。
* tinyexrを利用しています。
* https://github.com/syoyo/tinyexr
*/
class SpecularIblExrCubemapTexture {
public:
/**
* @brief テクスチャIDを取得
* @return テクスチャID
*/
const GLuint GetTextureId() const;
/**
* @brief デフォルトコンストラクタ
*
* 何もファイルを読み込まない空のテクスチャとなります。
* texture_id_は0で初期化されます。
*/
SpecularIblExrCubemapTexture();
/**
* @brief コンストラクタ
* @param path Cubemapテクスチャのディレクトリのパス
*/
SpecularIblExrCubemapTexture(const std::string& path);
/**
* @brief デストラクタ
*
* texture_id_のテクスチャを開放します。
*/
~SpecularIblExrCubemapTexture();
// コピー禁止
SpecularIblExrCubemapTexture(const SpecularIblExrCubemapTexture&) = delete;
SpecularIblExrCubemapTexture& operator=(const SpecularIblExrCubemapTexture&) =
delete;
/**
* @brief ムーブコンストラクタ
* @param other ムーブ元
*
* ムーブ後のtexture_id_は0に初期化されます。
*/
SpecularIblExrCubemapTexture(SpecularIblExrCubemapTexture&& other) noexcept;
/**
* @brief ムーブ代入演算子
* @param other ムーブ元
*
* ムーブ後のtexture_id_は0に初期化されます。
*/
SpecularIblExrCubemapTexture& operator=(
SpecularIblExrCubemapTexture&& other) noexcept;
private:
GLuint texture_id_;
};
} // namespace game
#endif // OPENGL_PBR_MAP_SPECULAR_IBL_EXR_CUBEMAP_TEXTURE_H_
const GLuint SpecularIblExrCubemapTexture::GetTextureId() const {
return texture_id_;
}
SpecularIblExrCubemapTexture::SpecularIblExrCubemapTexture() : texture_id_(0) {}
SpecularIblExrCubemapTexture::SpecularIblExrCubemapTexture(
const std::string& path)
: texture_id_(0) {
float* data;
const char* err = nullptr;
int width, height;
const std::array<std::string, 5> dirs = {
path + "/roughness-0", path + "/roughness-0-25",
path + "/roughness-0-5", path + "/roughness-0-75",
path + "/roughness-1",
};
std::array<std::string, 6> filenames = {"pos-x.exr", "neg-x.exr",
"pos-y.exr", "neg-y.exr",
"pos-z.exr", "neg-z.exr"};
glGenTextures(1, &texture_id_);
glBindTexture(GL_TEXTURE_CUBE_MAP, texture_id_);
for (unsigned int j = 0; j < 5; ++j) {
for (unsigned int i = 0; i < 6; ++i) {
const auto img_path = dirs[j] + "/" + filenames[i];
int ret = LoadEXR(&data, &width, &height, img_path.c_str(), &err);
if (ret != TINYEXR_SUCCESS) {
std::cerr << "Can't load image: " << img_path << std::endl;
if (err) {
std::cerr << "ERR : " << err << std::endl;
FreeEXRErrorMessage(err);
}
} else {
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, j, GL_RGBA32F, width,
height, 0, GL_RGBA, GL_FLOAT, data);
free(data);
}
}
if (j == 0) glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
}
SpecularIblExrCubemapTexture::~SpecularIblExrCubemapTexture() {
glDeleteTextures(1, &texture_id_);
}
SpecularIblExrCubemapTexture::SpecularIblExrCubemapTexture(
SpecularIblExrCubemapTexture&& other) noexcept
: texture_id_(other.texture_id_) {
other.texture_id_ = 0;
}
SpecularIblExrCubemapTexture& SpecularIblExrCubemapTexture::operator=(
SpecularIblExrCubemapTexture&& other) noexcept {
if (this != &other) {
glDeleteTextures(1, &texture_id_);
texture_id_ = other.texture_id_;
other.texture_id_ = 0;
}
return *this;
}
Sceneクラスの変更点
ヘッダを追加します。
#include "camera.h"
#include "directional_light.h"
#include "exr_cubemap_texture.h"
#include "exr_texture.h"
#include "mesh.h"
#include "mesh_entity.h"
#include "point_light.h"
#include "specular_ibl_exr_cubemap_texture.h"#include "spot_light.h"
シーンにlutとprefilteredテクスチャを追加します。
public:
std::unique_ptr<Camera> camera_;
std::vector<MeshEntity> mesh_entities_;
std::unique_ptr<DirectionalLight> directional_light_;
std::vector<PointLight> point_lights_;
std::vector<SpotLight> spot_lights_;
std::unique_ptr<ExrTexture> sky_texture_;
GLfloat sky_intensity_;
std::unique_ptr<ExrCubemapTexture> global_diffuse_ibl_texture_;
std::unique_ptr<SpecularIblExrCubemapTexture> global_specular_ibl_texture_; std::unique_ptr<ExrTexture> specular_ibl_lut_texture_;
Scene::LoadScene
に以下のように追記します。
...
// Global Diffuse IBL Texture
scene->global_diffuse_ibl_texture_ =
std::make_unique<ExrCubemapTexture>(path + "/GlobalIBL/Diffuse");
// Global Specular IBL Texture scene->global_specular_ibl_texture_ = std::make_unique<SpecularIblExrCubemapTexture>(path + "/GlobalIBL/Specular"); scene->specular_ibl_lut_texture_ = std::make_unique<ExrTexture>(path + "/GlobalIBL/Specular/lut.exr"); ...
SpecularIblPassクラスの作成
SpecularIblPassクラスを作成します。
#ifndef OPENGL_PBR_MAP_SPECULAR_IBL_PASS_PASS_H_
#define OPENGL_PBR_MAP_SPECULAR_IBL_PASS_PASS_H_
#include "create_program.h"
#include "scene.h"
namespace game {
class SpecularIblPass {
public:
/**
* @brief このパスをレンダリングする
*
* HDRバッファの輝度のLogを書き出します。
*/
void Render(const Scene& scene) const;
/**
* @brief コンストラクタ
* @param hdr_fbo HDRのフレームバッファオブジェクト
* @param gbuffer0 GBuffer0のテクスチャID
* @param gbuffer1 GBuffer1のテクスチャID
* @param gbuffer2 GBuffer2のテクスチャID
* @param fullscreen_vao 画面を覆うメッシュのVAO
*/
SpecularIblPass(const GLuint hdr_fbo, const GLuint gbuffer0,
const GLuint gbuffer1, const GLuint gbuffer2,
const GLuint fullscreen_vao);
/**
* @brief デストラクタ
*
* コンストラクタで生成したリソースを開放します。
*/
~SpecularIblPass();
private:
const GLuint hdr_fbo_;
const GLuint gbuffer0_;
const GLuint gbuffer1_;
const GLuint gbuffer2_;
const GLuint fullscreen_vao_;
const GLuint shader_program_;
const GLuint world_camera_pos_loc_;
const GLuint view_projection_i_loc_;
const GLuint projection_params_loc_;
};
} // namespace game
#endif // OPENGL_PBR_MAP_SPECULAR_IBL_PASS_PASS_H_
#include "specular_ibl_pass.h"
namespace game {
void SpecularIblPass::Render(const Scene& scene) const {
// Global Specular IBL
glUseProgram(shader_program_);
glBindFramebuffer(GL_FRAMEBUFFER, hdr_fbo_);
glDisable(GL_DEPTH_TEST);
const glm::vec3 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(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_);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_CUBE_MAP,
scene.global_specular_ibl_texture_->GetTextureId());
glActiveTexture(GL_TEXTURE4);
glBindTexture(GL_TEXTURE_2D, scene.specular_ibl_lut_texture_->GetTextureId());
glBindVertexArray(fullscreen_vao_);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
SpecularIblPass::SpecularIblPass(const GLuint hdr_fbo, const GLuint gbuffer0,
const GLuint gbuffer1, const GLuint gbuffer2,
const GLuint fullscreen_vao)
: hdr_fbo_(hdr_fbo),
gbuffer0_(gbuffer0),
gbuffer1_(gbuffer1),
gbuffer2_(gbuffer2),
fullscreen_vao_(fullscreen_vao),
shader_program_(CreateProgram("shader/SpecularIBLPass.vert",
"shader/SpecularIBLPass.frag")),
world_camera_pos_loc_(
glGetUniformLocation(shader_program_, "worldCameraPos")),
view_projection_i_loc_(
glGetUniformLocation(shader_program_, "ViewProjectionI")),
projection_params_loc_(
glGetUniformLocation(shader_program_, "ProjectionParams")) {}
SpecularIblPass::~SpecularIblPass() { glDeleteProgram(shader_program_); }
} // namespace game
SpecularIblPass用シェーダの作成
shader/SpecularIBLPass.vert
とshader/SpecularIBLPass.frag
を作成します。
#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);
}
#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;
layout (binding = 3) uniform samplerCube PrefilteredMap;
layout (binding = 4) uniform sampler2D lut;
uniform vec3 worldCameraPos;
uniform mat4 ViewProjectionI;
uniform vec2 ProjectionParams; // x: near, y: far
const float PI = 3.14159265358979323846;
const float HALF_MAX = 65504.0;
// 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;
}
// ###########################################################################
vec3 F_SchlickWithRoughness(vec3 F0, vec3 H, vec3 V, float roughness) {
return (F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - max(dot(V, H), 0), 5.0));
}
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 R = reflect(-V, N);
float NoV = dot(N, V);
vec3 F0 = mix(vec3(0.04), albedo, metallic);
vec3 prefilteredColor = clamp(textureLod(PrefilteredMap, R, roughness * 4).rgb, 0, HALF_MAX);
vec2 envBRDF = texture(lut, vec2(roughness, NoV)).rg;
vec3 specular = prefilteredColor * (F0 * envBRDF.x + envBRDF.y);
outRadiance = specular;
}
prefilteredマップから色を取得し、lutテクスチャからRoughnessとNoVで係数を取得して、specular成分を計算しています。
prefilteredColorはtextureLod
でLodの値を指定して取得しています。
SceneRendererクラスの変更点
SceneRendererにSpecularIblPassを追加します。
#include "diffuse_ibl_pass.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 "specular_ibl_pass.h"#include "spot_light_pass.h"
#include "tonemapping_pass.h"
GeometryPass geometry_pass_;
SkyPass sky_pass_;
DirectionalLightPass directional_light_pass_;
PointLightPass point_light_pass_;
SpotLightPass spot_light_pass_;
DiffuseIblPass diffuse_ibl_pass_;
SpecularIblPass specular_ibl_pass_; LogAveragePass log_average_pass_;
ExposurePass exposure_pass_;
TonemappingPass tonemapping_pass_;
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),
diffuse_ibl_pass_(hdr_fbo_, gbuffer0_, gbuffer1_, gbuffer2_,
fullscreen_mesh_vao_),
specular_ibl_pass_(hdr_fbo_, gbuffer0_, gbuffer1_, gbuffer2_, fullscreen_mesh_vao_), 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) {}
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);
diffuse_ibl_pass_.Render(scene);
specular_ibl_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();
}
Applicationクラスの変更点
Application::Init
でGL_TEXTURE_CUBE_MAP_SEAMLESS
を有効にします。
glEnable(GL_DEPTH_TEST);
glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
実行結果
実行結果は次のとおりです。
前々回まで使っていたファイルを使って改めて書き出してみると次のとおりです。
以前のものと比べると周囲のSkyから照らされて明るくなっています。
Blenderのビューポートレンダリングと比べると次の通りです。
Blenderの描画とも近くなっているのがわかります。
プログラム全文
プログラム全文はGitHubにアップロードしてあります。