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

はじめに

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

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

2020 04 18 21 26 27

Specular IBL

今回実装するSpecular IBLは次の資料に従って実装します。

DiffuseのIBLと同じようにSpecular成分について式を立ててみます。

Lo(p,v)=ΩBRDFspecularLi(p,l)(nl)dlL_o(p, v) = \int_{\Omega} BRDF_{specular} L_i(p, l) (n \cdot l) dl

ここで、BRDFspecularBRDF_{specular}は次の式です。

BRDFspecular=DFG4(nv)(nl)BRDF_{specular} = \frac{DFG}{4(n \cdot v)(n \cdot l)}

より詳しく書くと次のとおりです。

BRDFspecular(p,l,v)=D(h)F(v,h)G(l,v,h)4(nv)(nl)BRDF_{specular}(p, l, v) = \frac{D(h)F(v,h)G(l,v,h)}{4(n \cdot v)(n \cdot l)}

ppは反射の位置です。llはライトの方向ベクトル、vvはビューの方向ベクトル、hhはハーフベクトル、nnは法線ベクトルです。 あるllの方向からvvの方向に向かって反射する光を、半球のすべての方向からのllについて積分してvvに向かって反射する光というのを求めています。

ここでD(h)D(h)は次の式です。

D(h)=α2π((nh)2(α21)+1)2D(h) = \frac{\alpha^2}{\pi((n \cdot h)^2(\alpha^2 - 1) + 1)^2}

G(l,v,h)G(l,v,h)は次のとおりです。

k=(Roughness+1)28G1(v)=nv(nv)(1k)+kG(l,v,h)=G1(l)G1(v)k = \frac{(Roughness + 1)^2}{8}\\ G_1(v) = \frac{n \cdot v}{(n \cdot v)(1 - k) + k}\\ G(l, v, h) = G_1(l)G_1(v)

F(v,h)F(v, h)は先の資料では普通のSchlickの近似から微妙に変えたものを使っているようです。

F(v,h)=F0+(1F0)25.55473(vh)6.98316(vh)F(v, h) = F_0 + (1 - F_0)2^{-5.55473(v \cdot h) - 6.98316}(v \cdot h)

このBRDFspecularBRDF_{specular}は、定数だったBRDFdiffuse=ρπBRDF_{diffuse} = \frac{\rho}{\pi}と異なり、Viewの方向とLightの方向にも依存していることがわかります。 事前計算結果をキューブマップに焼き込んでNormalの方向からサンプリングすればよかったDiffuseのIBLと同じようには行きません。

とりあえずインテグラルを数値計算のモンテカルロ積分に置き換えます。

ΩBRDFspecular(p,l,v)Li(p,l)(nl)dl1Nk=1NBRDFspecular(p,lk,v)Li(p,lk)(nlk)p(lk,v)\int_{\Omega}BRDF_{specular}(p, l, v) L_i(p, l) (n \cdot l) dl \simeq \frac{1}{N}\sum_{k=1}^N \frac{BRDF_{specular}(p, l_k, v) L_i(p, l_k) (n \cdot l_k)}{p(l_k, v)}

分母のp(lk,v)p(l_k, v)は確率密度関数(pdf)です。 モンテカルロ積分のインポータンスサンプリングというものになっています。 これについては次のページなどがわかりやすいです。

スペキュラ反射は鏡面反射の方向ほど影響度が強いピークのある関数になっています。 こういう偏った関数をモンテカルロ積分で求める場合は、uniformな確率でランダムにサンプリングしても良いのですが、関数に合わせた形の確率でサンプリングしたほうが少ないサンプル数で精度が上がります。

さて、このモンテカルロによる近似式をさらに大胆に2つに分割するという近似を行います。

1Nk=1NBRDFspecular(p,lk,v)Li(p,lk)(nlk)p(lk,v)(1Nk=1NLi(p,lk))(1Nk=1NBRDFspecular(p,lk,v)(nlk)p(lk,v))\frac{1}{N}\sum_{k=1}^N \frac{BRDF_{specular}(p, l_k, v) L_i(p, l_k) (n \cdot l_k)}{p(l_k, v)}\\ \simeq (\frac{1}{N}\sum_{k=1}^N L_i(p, l_k)) (\frac{1}{N}\sum_{k=1}^N \frac{BRDF_{specular}(p, l_k, v) (n \cdot l_k)}{p(l_k, v)})

ここで、前半のライトの部分について事前計算をしてキューブマップに収めます。 lkl_kというのはインポータンスサンプリングの確率によってサンプリング点を求めるわけですが、インポータンスサンプリングに使う確率はRoughnessに応じて反射のピークの形が変わるのでそれに合わせて変更しています。 そこで、いくつかのRoughness値について事前計算をする必要があります。 Roughnessが粗くなるにつれて反射はぼやけたものになるので、Roughnessが低い値では高周波成分が必要ですが粗くなると小さい画像に格納しても問題なくなります。 これはキューブマップのmipmapに入れるのに適していることになります。 今回はRoughnessが0、0.25、0.5、0.75、1.0の場合をmipmapに格納し、その間の値についてはmipmapのブレンドを使うことにします。

本来、マイクロファセットベースのGGXの分布は面に対する視点の方向によって変化するはずですが、ここではn=v=rn = v = rのような場合と仮定して近似的に求めてしまうことにします。 この近似によってグレージング角付近のスペキュラの引き伸ばされたような反射は再現できなくなります。

2020 04 18 13 38 51

(画像は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で収めるということで説明しました。 つぎに後半の部分について説明します。

後半の方の式はF0F0について整理すると次のようになります。

ΩBRDFspecular(p,l,v)(nl)dl=F0ΩBRDFspecular(p,l,v)F(v,h)(1(1vh)5)(nl)dl+ΩBRDFspecular(p,l,v)F(v,h)(1vh)5(nl)dl\int_{\Omega} BRDF_{specular}(p, l, v) (n \cdot l) dl = F0 \int_{\Omega}\frac{BRDF_{specular}(p, l, v)}{F(v, h)}(1 - (1 - v \cdot h)^5) (n \cdot l) dl\\ + \int_{\Omega}\frac{BRDF_{specular}(p, l, v)}{F(v, h)}(1 - v \cdot h)^5 (n \cdot l) dl

このようにF0F0について整理してやると、RoughnessRoughness(nl)(n \cdot l)の2つのinputからF0F0に対するスケール(ΩBRDFspecular(p,l,v)F(v,h)(1(1vh)5)(nl)dl\int_{\Omega}\frac{BRDF_{specular}(p, l, v)}{F(v, h)}(1 - (1 - v \cdot h)^5) (n \cdot l) dl)とバイアス(ΩBRDFspecular(p,l,v)F(v,h)(1vh)5(nl)dl\int_{\Omega}\frac{BRDF_{specular}(p, l, v)}{F(v, h)}(1 - v \cdot h)^5 (n \cdot l) dl)の2つのoutputを事前計算してやれば良さそうです。 幸いなことにすべて[0, 1]区間なのでテクスチャに焼き込むことができます。 2つのinputである[0, 1]のRoughnessRoughness(nv)(n \cdot v)をテクスチャの縦と横に割り当てて、そこから求められるF0F0に対するスケールとバイアスとをテクスチャのRG成分に書き出してやって、2DのLook-up texture(LUT)として使います。

2020 04 18 13 49 02

(図は先の資料より。cosθv\cos\theta_vというのは(nv)(n \cdot v)のことです)

Specular IBLの事前計算用プログラムの作成

IBL-Specularというプロジェクトを新しく用意します。

2020 04 18 13 58 56

GLFWやGLEW、tinyexrなどをプロジェクトから使えるようにセットアップします。

main.cppに全部書いていくことにします。

まずはヘッダのインクルードと、exrの読み込み保存、ShaderProgramの作成やGLFWの初期化処理などです。

main.cpp
#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に続けて書いていきます。

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.vertIBLSpecularShader/Lut.fragは次のとおりです。

IBLSpecularShader/Lut.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);
}
IBLSpecularShader/Lut.frag
#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);
}

上で説明した数式通りにプログラムを書いています。 RoughnessRoughness(nv)(n \cdot v)をUVから決めています。

Prefiletered Mapを作成するプログラム

次にprefiltered mapを計算するプログラムです。 これもmain.cppに追記していきます。

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.vertIBLSpecularShader/shader.fragは次のとおりです。

IBLSpecularShader/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);
}
IBLSpecularShader/shader.frag
#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__.pyscene_exporter.pyと同じディレクトリにIBL-Specular.exeIBLSpecularShader/を配置します。

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

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用のテクスチャが出力されます。

2020 04 18 19 25 35

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

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

SpecularIblExrCubemapTextureクラスの作成

mipmapに各Roughnessのキューブマップテクスチャを読み込むクラスを作成します。

specular_ibl_exr_cubemap_texture.h
#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_
ext_texture.cpp
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クラスの変更点

ヘッダを追加します。

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 "specular_ibl_exr_cubemap_texture.h"#include "spot_light.h"

シーンにlutとprefilteredテクスチャを追加します。

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_;
  std::unique_ptr<SpecularIblExrCubemapTexture> global_specular_ibl_texture_;  std::unique_ptr<ExrTexture> specular_ibl_lut_texture_;

Scene::LoadSceneに以下のように追記します。

scene.cpp
  ...
  // 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クラスを作成します。

specular_ibl_pass.h
#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_
specular_ibl_pass.cpp
#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.vertshader/SpecularIBLPass.fragを作成します。

shader/SpecularIBLPass.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/SpecularIBLPass.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 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を追加します。

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 "specular_ibl_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_;
  SpecularIblPass specular_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_),
      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) {}
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);

  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::InitGL_TEXTURE_CUBE_MAP_SEAMLESSを有効にします。

application.cpp
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);

実行結果

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

2020 04 18 21 26 27

前々回まで使っていたファイルを使って改めて書き出してみると次のとおりです。

2020 04 18 21 28 34

以前のものと比べると周囲のSkyから照らされて明るくなっています。

Blenderのビューポートレンダリングと比べると次の通りです。

2020 04 18 21 29 49

Blenderの描画とも近くなっているのがわかります。

プログラム全文

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

GitHub: MatchaChoco010/OpenGL-PBR-Map at v22

新しい投稿
地下駐車場