OpenGLでDeferredシェーディングを実装する(Diffuse IBL)
はじめに
この記事はシリーズ記事です。目次はこちら。
この記事ではPBRの古典的DeferredシェーディングにDiffuseのIBLを追加しようと思います。
Blendファイルの用意
次のようなシーンをBlenderで作成します。
次のページからダウンロードしたファイルを利用しました。
ダウンロードしたファイルは.hdr形式なため.exr形式に変換しました。
シーンファイルの書き出しにDirectionalLightが必須のため配置してありますが、強さは0に設定してあります。
上段が金属で下段が非金属の各metallicの値によるマテリアルが設定してあります。
このシーンを前回までのプログラムで読み込んで表示してみると次のようになります。
IBLが実装されていないため、オブジェクトはSkyから照らされることはなく、直接光のライトは強さ0のDirectional Lightしか配置していないのでオブジェクトが真っ暗になっています。 IBLの実装を2回に分けて行っていきます。 今回はIBLのDiffuse成分を実装します。
Irradiance Map
今回はBlendファイルからシーンを書き出す際にIrradianceMap(放射照度マップ)を書き出すようにします。 放射照度マップについては次のページが良い資料です。
数式で書いてみると、今回求めたいのは半球からのすべてのライトによるdiffuse項なので次のようにかけます。
ここではカメラの方向で、はある反射した点からカメラ方向への放射輝度です。
はフレネルなどから求まるdiffuse成分の割合です。 前回までのPBRシェーダでもコード中にkDというのがあったと思います。
次に積分とBRDFが出てきます。 がコサイン項ですね。
今回はBRDFは定数を使っていたので積分の外に出せます。
ここで後半の放射照度に対応するの部分を事前計算してマップに焼き込むことにします。
天球を方位角と仰角とを使って表現すると次のようになります。
これを数値的に解くためにリーマン積分で書き直すと次のようになります。
この式をもとに各法線における放射照度を計算し、キューブマップに格納して使います。
Irradiance Mapを書き出すプログラムの作成
新しいC++のプロジェクトを作成します。 glewやglfw、glm、tinyexrを読み込むようにセットします。
main.cpp
main.cppに全てを詰め込みました。
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <array>
#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 auto width = 32;
const auto height = 32;
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(32, 32, "", 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;
}
void IrradianceMap(const std::string& input_path, const float sky_intensity,
const std::string& output_path) {
std::cout << "input path: " << input_path << std::endl;
std::cout << "output path: " << output_path << std::endl;
const auto sky_exr = LoadExr(input_path);
GLuint mesh_vao;
glGenVertexArrays(1, &mesh_vao);
glBindVertexArray(mesh_vao);
const std::array<glm::vec2, 3> mesh_vertices = {
glm::vec2(-1.0, -1.0),
glm::vec2(3.0, -1.0),
glm::vec2(-1.0, 3.0),
};
GLuint mesh_vertices_vbo;
glGenBuffers(1, &mesh_vertices_vbo);
glBindBuffer(GL_ARRAY_BUFFER, mesh_vertices_vbo);
glBufferData(GL_ARRAY_BUFFER, 3 * sizeof(glm::vec2), &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);
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 32, 32, 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);
GLuint fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
texture, 0);
glViewport(0, 0, 32, 32);
const auto program = CreateProgram("IBLDiffuseShader/shader.vert",
"IBLDiffuseShader/shader.frag");
const auto rotation_loc = glGetUniformLocation(program, "Rotation");
const auto sky_intensity_loc = glGetUniformLocation(program, "skyIntensity");
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, 6> output_paths = {
output_path + "/" + "pos-x.exr", output_path + "/" + "neg-x.exr",
output_path + "/" + "pos-y.exr", output_path + "/" + "neg-y.exr",
output_path + "/" + "pos-z.exr", output_path + "/" + "neg-z.exr",
};
glUseProgram(program);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, sky_exr);
glUniform1f(sky_intensity_loc, sky_intensity);
for (int i = 0; i < 6; ++i) {
glUniformMatrix4fv(rotation_loc, 1, GL_FALSE, &rotations[i][0][0]);
glBindVertexArray(mesh_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
std::unique_ptr<float[]> data = std::make_unique<float[]>(4 * 32 * 32);
glReadPixels(0, 0, 32, 32, GL_RGBA, GL_FLOAT, data.get());
SaveExr(std::move(data), output_paths[i]);
}
}
int main(int argc, char* argv[]) {
if (argc <= 3) {
std::cerr << "src and dst path is required." << std::endl;
return -1;
}
Init();
IrradianceMap(std::string(argv[1]), std::stof(std::string(argv[2])),
std::string(argv[3]));
}
長いですがメインの処理はIrradianceMap
です。
IrradianceMap
の内部では描画に使う全画面を覆うメッシュと書き出し用のfboを用意しています。
書き出しのテクスチャのサイズは32x32で固定です。
shader
IBLDiffuseShader/shader.vert
とIBLDiffuseShader/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 vec3 outputColor;
layout (binding = 0) uniform sampler2D SkyImage;
uniform float skyIntensity;
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;
}
// ###########################################################################
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;
}
void main() {
vec3 normal = normalize(direction);
vec3 irradiance = vec3(0.0);
vec3 up = vec3(0.0, 1.0, 0.0);
vec3 right = cross(up, normal);
up = cross(normal, right);
for (int i = 0; i < 256; i++) {
for (int j = 0; j < 64; j++) {
float phi = 2 * PI * float(i) / 256.0;
float theta = 0.5 * PI * float(j) / 64.0;
vec3 tangentSampleVec = vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
vec3 sampleVec = tangentSampleVec.x * right + tangentSampleVec.y * normal + tangentSampleVec.z * up;
irradiance += SampleColor(sampleVec) * cos(theta) * sin(theta);
}
}
irradiance = PI * PI * irradiance / float(256 * 64);
outputColor = irradiance;
}
上で説明した数式の通りリーマン積分を計算しています。
試しにIrradiance Mapを作成する
リリースビルドをして、IBL-Diffuse.exe
のあるディレクトリにIBLDiffuseShaderディレクトリとLA_Downtown_Afternoon_Fishing_3k.exr
を配置します。
そして次のようにしてskyIntensityを1で呼び出してみます。
$ ./IBL-Diffuse.exe ./LA_Downtown_Afternoon_Fishing_3k.exr 1 .
実行すると次のようなログが流れます。
input path: ./LA_Downtown_Afternoon_Fishing_3k.exr
output path: .
Saved exr file. ./pos-x.exr
Saved exr file. ./neg-x.exr
Saved exr file. ./pos-y.exr
Saved exr file. ./neg-y.exr
Saved exr file. ./pos-z.exr
Saved exr file. ./neg-z.exr
生成されたexrファイルをつなぎ合わせてみると次の通り。
全体的に明るくぼやけたようなIrradiance Mapが出力されています。
Blender Addonの変更点
__init__.py
やscene_exporter.py
と同じディレクトリにIBL-Diffuse.exe
とIBLDiffuseShader/
を配置します。
scene_exporter.py
を次のように変更します。
def execute(self, context):
pwd = os.getcwd() os.chdir(os.path.dirname(__file__))
print(self.filepath)
...
sky += "SkyEnd\n"
# 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])
scene_text = "# Scene file\n"
...
with open(os.path.join(self.filepath, "scenefile.txt"), mode="w") as f:
f.write(scene_text)
os.chdir(pwd)
return {"FINISHED"}
このアドオンでシーンファイルを出力するとシーンファイルのディレクトリの中にGlobalIBL/Diffuse/
ディレクトリが作られ、その中にキューブマップのテクスチャが格納されます。
前回のプログラムからの変更点
前回のプログラムからの変更点を次に示します。
ExrCubemapTextureクラスの作成
exr_cubemap_texture.h
を作成し、exr_texture.cpp
に追記をします。
#ifndef OPENGL_PBR_MAP_EXR_CUBEMAP_TEXTURE_H_
#define OPENGL_PBR_MAP_EXR_CUBEMAP_TEXTURE_H_
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <array>
#include <iostream>
#include <string>
namespace game {
/**
* @brief .exr形式のCubemapテクスチャ
*
* OpenEXR形式のテクスチャを読み込みOpenGLにアップロードするクラスです。
* テクスチャの多重開放を避けるためコピー禁止です。
* ムーブは可能です。
* tinyexrを利用しています。
* https://github.com/syoyo/tinyexr
*/
class ExrCubemapTexture {
public:
/**
* @brief テクスチャIDを取得
* @return テクスチャID
*/
const GLuint GetTextureId() const;
/**
* @brief デフォルトコンストラクタ
*
* 何もファイルを読み込まない空のテクスチャとなります。
* texture_id_は0で初期化されます。
*/
ExrCubemapTexture();
/**
* @brief コンストラクタ
* @param path Cubemapテクスチャのディレクトリのパス
*/
ExrCubemapTexture(const std::string& path);
/**
* @brief デストラクタ
*
* texture_id_のテクスチャを開放します。
*/
~ExrCubemapTexture();
// コピー禁止
ExrCubemapTexture(const ExrCubemapTexture&) = delete;
ExrCubemapTexture& operator=(const ExrCubemapTexture&) = delete;
/**
* @brief ムーブコンストラクタ
* @param other ムーブ元
*
* ムーブ後のtexture_id_は0に初期化されます。
*/
ExrCubemapTexture(ExrCubemapTexture&& other) noexcept;
/**
* @brief ムーブ代入演算子
* @param other ムーブ元
*
* ムーブ後のtexture_id_は0に初期化されます。
*/
ExrCubemapTexture& operator=(ExrCubemapTexture&& other) noexcept;
private:
GLuint texture_id_;
};
} // namespace game
#endif // OPENGL_PBR_MAP_EXR_CUBEMAP_TEXTURE_H_
const GLuint ExrCubemapTexture::GetTextureId() const { return texture_id_; }
ExrCubemapTexture::ExrCubemapTexture() : texture_id_(0) {}
ExrCubemapTexture::ExrCubemapTexture(const std::string& path) : texture_id_(0) {
float* data;
const char* err = nullptr;
int width, height;
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_);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_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);
for (unsigned int i = 0; i < 6; ++i) {
const auto img_path = path + "/" + 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, 0, GL_RGBA16F, width,
height, 0, GL_RGBA, GL_FLOAT, data);
free(data);
}
}
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
}
ExrCubemapTexture::~ExrCubemapTexture() { glDeleteTextures(1, &texture_id_); }
ExrCubemapTexture::ExrCubemapTexture(ExrCubemapTexture&& other) noexcept
: texture_id_(other.texture_id_) {
other.texture_id_ = 0;
}
ExrCubemapTexture& ExrCubemapTexture::operator=(
ExrCubemapTexture&& other) noexcept {
if (this != &other) {
glDeleteTextures(1, &texture_id_);
texture_id_ = other.texture_id_;
other.texture_id_ = 0;
}
return *this;
}
} // namespace game
Sceneクラスの変更点
Global Diffuse IBL用のテクスチャを保持します。
#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 "spot_light.h"
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<Scene> Scene::LoadScene(const std::string path,
const GLuint width,
const GLuint height) {
auto scene = std::make_unique<Scene>();
auto scenefile_txt_path = path + "/scenefile.txt";
std::ifstream ifs(scenefile_txt_path);
std::string line;
if (ifs.fail()) {
std::cerr << "Can't open scene file: " << path << std::endl;
return scene;
}
std::unordered_map<std::string, std::shared_ptr<Mesh>> tmp_mesh_map;
std::unordered_map<std::string, std::shared_ptr<Material>> tmp_material_map;
// Global Diffuse IBL Texture scene->global_diffuse_ibl_texture_ = std::make_unique<ExrCubemapTexture>(path + "/GlobalIBL/Diffuse"); ...
DiffuseIblPassクラスの作成
DiffuseのIBLを計算するパスを追加します。
#ifndef OPENGL_PBR_MAP_DIFFUSE_IBL_PASS_PASS_H_
#define OPENGL_PBR_MAP_DIFFUSE_IBL_PASS_PASS_H_
#include "create_program.h"
#include "scene.h"
namespace game {
class DiffuseIblPass {
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
*/
DiffuseIblPass(const GLuint hdr_fbo, const GLuint gbuffer0,
const GLuint gbuffer1, const GLuint gbuffer2,
const GLuint fullscreen_vao);
/**
* @brief デストラクタ
*
* コンストラクタで生成したリソースを開放します。
*/
~DiffuseIblPass();
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_DIFFUSE_IBL_PASS_PASS_H_
#include "diffuse_ibl_pass.h"
namespace game {
void DiffuseIblPass::Render(const Scene& scene) const {
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_diffuse_ibl_texture_->GetTextureId());
glBindVertexArray(fullscreen_vao_);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
DiffuseIblPass::DiffuseIblPass(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/DiffuseIBLPass.vert",
"shader/DiffuseIBLPass.frag")),
world_camera_pos_loc_(
glGetUniformLocation(shader_program_, "worldCameraPos")),
view_projection_i_loc_(
glGetUniformLocation(shader_program_, "ViewProjectionI")),
projection_params_loc_(
glGetUniformLocation(shader_program_, "ProjectionParams")) {}
DiffuseIblPass::~DiffuseIblPass() { glDeleteProgram(shader_program_); }
} // namespace game
画面全体を覆うメッシュを使って描画をしています。
テクスチャの3番目として計算したIrradianceMapのキューブマップを渡しています。
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_CUBE_MAP,
scene.global_diffuse_ibl_texture_->GetTextureId());
DiffuseIBLPassのシェーダの作成
shader/DiffuseIBLPass.vert
とshader/DiffuseIBLPass.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 IrradianceMap;
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;
}
// ###########################################################################
// BRDF ######################################################################
vec3 NormalizedLambert(vec3 diffuseColor) {
return diffuseColor / PI;
}
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 F0 = mix(vec3(0.04), albedo, metallic);
vec3 kS = F_SchlickWithRoughness(F0, N, V, roughness);
vec3 kD = 1.0 - kS;
kD *= 1.0 - metallic;
vec3 irradiance = clamp(texture(IrradianceMap, N).rgb, 0, HALF_MAX);
vec3 diffuse = kD * NormalizedLambert(albedo) * irradiance;
outRadiance = diffuse;
}
ここではハーフベクトルの代わりに法線を使っています。 ハーフベクトルはライトベクトルが定まらないので決めることはできません。
次のページで解説されているようにしてラフネス項をフレネルに追加しています。
Adopting a physically based shading model | Sébastien Lagarde
Diffuse項の係数kD
を計算して照度と正規化ランバートをかけてIrradianceMapから反射しカメラに向かう輝度とします。
金属はkD
が0になるっぽいです?
SceneRendererクラスの変更点
DiffuseIblPassをSceneRendererに追加します。
#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 "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_; 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_), 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);
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();
}
実行結果
実行結果は次のとおりです。
金属はdiffuse反射がない?ため真っ黒です。
非金属はdiffuseで照らされて明るくなりました。
diffuse反射のみでspecular反射はまだ実装していないため、景色の映り込みのような鋭い反射はありません。 Roughnessの違いもわかりにくいものとなっています。
プログラム全文
プログラム全文はGitHubにアップロードしてあります。