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

はじめに

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

この記事ではPBRの古典的DeferredシェーディングにDiffuseのIBLを追加しようと思います。

2020 04 15 12 37 50

Blendファイルの用意

次のようなシーンをBlenderで作成します。

2020 04 14 10 31 57

次のページからダウンロードしたファイルを利用しました。

ダウンロードしたファイルは.hdr形式なため.exr形式に変換しました。

シーンファイルの書き出しにDirectionalLightが必須のため配置してありますが、強さは0に設定してあります。

上段が金属で下段が非金属の各metallicの値によるマテリアルが設定してあります。

このシーンを前回までのプログラムで読み込んで表示してみると次のようになります。

2020 04 14 10 31 36

IBLが実装されていないため、オブジェクトはSkyから照らされることはなく、直接光のライトは強さ0のDirectional Lightしか配置していないのでオブジェクトが真っ暗になっています。 IBLの実装を2回に分けて行っていきます。 今回はIBLのDiffuse成分を実装します。

Irradiance Map

今回はBlendファイルからシーンを書き出す際にIrradianceMap(放射照度マップ)を書き出すようにします。 放射照度マップについては次のページが良い資料です。

数式で書いてみると、今回求めたいのは半球からのすべてのライトによるdiffuse項なので次のようにかけます。

Lo(p,ωo)=kdΩBRDFdiffuseLi(p,ωi)(nωi)dωiL_o(\overrightarrow{p}, \overrightarrow{\omega_o}) = k_d \cdot \int_{\Omega} BRDF_{diffuse} \cdot L_i(\overrightarrow{p}, \overrightarrow{\omega_i}) \cdot (\overrightarrow{n} \cdot \overrightarrow{\omega_i}) d\overrightarrow{\omega_i}

ここでωo\overrightarrow{\omega_o}はカメラの方向で、Lo(p,ωo)L_o(\overrightarrow{p}, \overrightarrow{\omega_o})はある反射した点p\overrightarrow{p}からカメラ方向への放射輝度です。

kdk_dはフレネルなどから求まるdiffuse成分の割合です。 前回までのPBRシェーダでもコード中にkDというのがあったと思います。

次に積分とBRDFが出てきます。 (nωi)(\overrightarrow{n} \cdot \overrightarrow{\omega_i})がコサイン項ですね。

今回はBRDFは定数ρdπ\frac{\rho_d}{\pi}を使っていたので積分の外に出せます。

Lo(p,ωo)=kdρdπΩLi(p,ωi)(nωi)dωiL_o(\overrightarrow{p}, \overrightarrow{\omega_o}) = k_d \cdot \frac{\rho_d}{\pi} \cdot \int_{\Omega}L_i(\overrightarrow{p}, \overrightarrow{\omega_i}) \cdot (\overrightarrow{n} \cdot \overrightarrow{\omega_i}) d\overrightarrow{\omega_i}

ここで後半の放射照度に対応するΩLi(p,ωi)(nωi)dωi\int_{\Omega}L_i(\overrightarrow{p}, \overrightarrow{\omega_i}) \cdot (\overrightarrow{n} \cdot \overrightarrow{\omega_i}) d\overrightarrow{\omega_i}の部分を事前計算してマップに焼き込むことにします。

天球Ω\Omegaを方位角と仰角ϕ\phiθ\thetaを使って表現すると次のようになります。

ϕ=02πθ=0π2Li(p,ϕ,θ)cos(θ)sin(θ)dϕdθ\int_{\phi = 0}^{2\pi} \int_{\theta = 0}^{\frac{\pi}{2}} L_i(p, \phi, \theta) \cos(\theta) \sin(\theta) d\phi d\theta

これを数値的に解くためにリーマン積分で書き直すと次のようになります。

π2n1n2ϕ=0n1θ=0n2Li(p,ϕ,θ)cos(θ)sin(θ)dϕdθ\frac{\pi^2}{n_1 n_2}\sum_{\phi = 0}^{n_1}\sum_{\theta = 0}^{n_2} L_i(p, \phi, \theta) \cos(\theta) \sin(\theta) d\phi d\theta

この式をもとに各法線n\overrightarrow{n}における放射照度を計算し、キューブマップに格納して使います。

Irradiance Mapを書き出すプログラムの作成

新しいC++のプロジェクトを作成します。 glewやglfw、glm、tinyexrを読み込むようにセットします。

main.cpp

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.vertIBLDiffuseShader/shader.fragを作成します。

IBLDiffuseShader/shader.vert
#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);
}
IBLDiffuseShader/shader.frag
#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ファイルをつなぎ合わせてみると次の通り。

2020 04 14 15 11 37

全体的に明るくぼやけたようなIrradiance Mapが出力されています。

Blender Addonの変更点

__init__.pyscene_exporter.pyと同じディレクトリにIBL-Diffuse.exeIBLDiffuseShader/を配置します。

2020 04 14 15 35 11

scene_exporter.pyを次のように変更します。

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に追記をします。

exr_cubemap_texture.h
#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_
exr_texture.cpp
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用のテクスチャを保持します。

scene.h
#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"
scene.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_;
scene.cpp
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を計算するパスを追加します。

diffuse_ibl_pass.h
#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_
diffuse_ibl_pass.cpp
#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.vertshader/DiffuseIBLPass.fragを作ります。

shader/DiffuseIBLPass.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/DiffuseIBLPass.frag
#version 460

in vec2 vUv;

layout (location = 0) out vec3 outRadiance;

layout (binding = 0) uniform sampler2D GBuffer0;
layout (binding = 1) uniform sampler2D GBuffer1;
layout (binding = 2) uniform sampler2D GBuffer2;
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に追加します。

scene_renderer.h
#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"
scene_renderer.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_;
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),
      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) {}
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);

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

実行結果

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

2020 04 15 12 37 50

金属はdiffuse反射がない?ため真っ黒です。

非金属はdiffuseで照らされて明るくなりました。

diffuse反射のみでspecular反射はまだ実装していないため、景色の映り込みのような鋭い反射はありません。 Roughnessの違いもわかりにくいものとなっています。

プログラム全文

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

GitHub: MatchaChoco010/OpenGL-PBR-Map at v21