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

はじめに

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

この記事ではPBRの古典的DeferredシェーディングでSkyを描画してみます。

2020 04 12 15 23 36

Sky

前回まではオブジェクトが描画されない部分は画面クリアした黒で描画されたままになっていました。 今回はその部分に空を描画してみます。

Blenderアドオンの変更点

Blendファイルに空を追加します。

BackgroundにEnvirounmentなImageを追加します。 Equirectangularな画像を追加しました。 これは縦横1:2のHDRi画像を設定するものです。

2020 04 12 14 22 26

今回は次のような.exrファイルを用意しました。 .exrというのはHDRを扱える画像形式の一つです。

2020 04 12 14 23 13

この画像はKritaというペイントソフトで作成したものです。 KritaにはHDRな画像をペイントする機能があるので今回はKritaを使いましたが、好きなペイントソフトを使えば良いと思います。

Blendファイルに設定してみると次のとおりです。

2020 04 12 14 23 25

環境光成分に環境テクスチャが使われるようで、、シーン全体が明るくなりました。

このSkyの画像をBlendファイルからSceneFileに書き出すようにアドオンを変更します。

scenefile_exporter.py
    def execute(self, context):
        print(self.filepath)
        if os.path.exists(self.filepath):
            raise Exception("Already exists {0}".format(self.filepath))
        os.makedirs(self.filepath)

        scene_collection = bpy.data.collections['Scene']

        meshes_dict = {}
        materials_dict = {}
        mesh_entities = []
        directional_light = ""
        point_lights = []
        spot_lights = []
        sky = ""
        ...

        # Sky        sky_image = bpy.data.worlds["World"].node_tree.nodes["Environment Texture"].image        os.makedirs(os.path.join(self.filepath, "Sky"))        sky_ext = os.path.splitext(sky_image.filepath)[1]        if sky_ext != ".exr":            raise Exception("Environment Texture must be .exr format.")        shutil.copy2(            bpy.path.abspath(sky_image.filepath),            os.path.join(self.filepath, "Sky", "sky.exr")        )        sky_intensity = bpy.data.worlds["World"].node_tree.nodes["Background"].inputs[1].default_value * 683.002        sky += "Sky:\n"        sky += "SkyImagePath: Sky/sky.exr\n"        sky += "skyIntensity: {0}\n".format(sky_intensity)        sky += "SkyEnd\n"
        scene_text = "# Scene file\n"
        for mesh_text in meshes_dict.values():
            scene_text += mesh_text
        for material_text in materials_dict.values():
            scene_text += material_text
        for mesh_entity in mesh_entities:
            scene_text += mesh_entity
        scene_text += directional_light
        for point_light in point_lights:
            scene_text += point_light
        for spot_light in spot_lights:
            scene_text += spot_light
        scene_text += sky
        with open(os.path.join(self.filepath, "scenefile.txt"), mode="w") as f:
            f.write(scene_text)

        return {"FINISHED"}

Blender側は.exr以外の様々なファイル形式に対応していますが、今回作成するプログラムでは簡単のためSkyテクスチャは.exr決め打ちで実装しようと思います。

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

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

tinyexrの追加

まずはexrファイルの読み込み用にtinyexrというライブラリをvendorsに追加します。

syoyo/tinyexr: Tiny OpenEXR image loader/saver library

ExrTextureクラスの作成

.exrファイルを読み込み保持するクラスを作成します。

exr_texture.h
#ifndef OPENGL_PBR_MAP_EXR_TEXTURE_H_
#define OPENGL_PBR_MAP_EXR_TEXTURE_H_

#include <GL/glew.h>
#include <GLFW/glfw3.h>

#include <iostream>
#include <string>

namespace game {

/**
 * @brief .exr形式のテクスチャ
 *
 * OpenEXR形式のテクスチャを読み込みOpenGLにアップロードするクラスです。
 * テクスチャの多重開放を避けるためコピー禁止です。
 * ムーブは可能です。
 * tinyexrを利用しています。
 * https://github.com/syoyo/tinyexr
 */
class ExrTexture {
 public:
  /**
   * @brief テクスチャの幅を取得
   * @return テクスチャの幅
   */
  const int GetWidth() const;

  /**
   * @brief テクスチャの高さを取得
   * @return テクスチャの高さ
   */
  const int GetHeight() const;

  /**
   * @brief テクスチャIDを取得
   * @return テクスチャID
   */
  const GLuint GetTextureId() const;

  /**
   * @brief デフォルトコンストラクタ
   *
   * 何もファイルを読み込まない空のテクスチャとなります。
   * texture_id_は0で初期化されます。
   */
  ExrTexture();

  /**
   * @brief コンストラクタ
   * @param path テクスチャのパス
   * @param sRGB sRGBならばtrue、Linearならばfalse
   */
  ExrTexture(const std::string& path);

  /**
   * @brief デストラクタ
   *
   * texture_id_のテクスチャを開放します。
   */
  ~ExrTexture();

  // コピー禁止
  ExrTexture(const ExrTexture&) = delete;
  ExrTexture& operator=(const ExrTexture&) = delete;

  /**
   * @brief ムーブコンストラクタ
   * @param other ムーブ元
   *
   * ムーブ後のtexture_id_は0に初期化されます。
   */
  ExrTexture(ExrTexture&& other) noexcept;

  /**
   * @brief ムーブ代入演算子
   * @param other ムーブ元
   *
   * ムーブ後のtexture_id_は0に初期化されます。
   */
  ExrTexture& operator=(ExrTexture&& other) noexcept;

 private:
  int width_;
  int height_;
  int channel_;
  GLuint texture_id_;
};

}  // namespace game

#endif  // OPENGL_PBR_MAP_EXR_TEXTURE_H_
exr_texture.cpp
#include "exr_texture.h"

#define TINYEXR_IMPLEMENTATION
#include <tinyexr.h>

namespace game {

const int ExrTexture::GetWidth() const { return width_; }

const int ExrTexture::GetHeight() const { return height_; }

const GLuint ExrTexture::GetTextureId() const { return texture_id_; }

ExrTexture::ExrTexture() : width_(0), height_(0), channel_(0), texture_id_(0) {}

ExrTexture::ExrTexture(const std::string& path) : channel_(4), texture_id_(0) {
  float* data;
  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);
  }
}

ExrTexture::~ExrTexture() { glDeleteTextures(1, &texture_id_); }

ExrTexture::ExrTexture(ExrTexture&& other) noexcept
    : texture_id_(other.texture_id_),
      width_(other.width_),
      height_(other.height_),
      channel_(other.channel_) {
  other.texture_id_ = 0;
}

ExrTexture& ExrTexture::operator=(ExrTexture&& other) noexcept {
  if (this != &other) {
    glDeleteTextures(1, &texture_id_);
    texture_id_ = other.texture_id_;
    other.texture_id_ = 0;

    width_ = other.width_;
    height_ = other.height_;
    channel_ = other.channel_;
  }

  return *this;
}

}

Textureクラスと同様にコピー禁止です。

Sceneクラスの変更

SceneクラスにSkyテクスチャを保持するように変更します。

scene.h
#include "camera.h"
#include "directional_light.h"
#include "mesh.h"
#include "mesh_entity.h"
#include "point_light.h"
#include "spot_light.h"
#include "exr_texture.h"

namespace game {

class Scene {
 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_;

...

Scene::LoadSceneで書き出したSkyテクスチャを読み込むようにします。

scene.cpp
...

    // Sky
    else if (texts[0] == "Sky:") {
      std::string sky_image_path;
      GLfloat sky_intensity;
      while (std::getline(ifs, line)) {
        auto texts = SplitString(line, ' ');

        if (texts[0] == "SkyImagePath:") {
         sky_image_path = path + "/" + texts[1];
        }

        if (texts[0] == "skyIntensity:") {
          sky_intensity = std::stof(texts[1]);
        }

        if (line == "SkyEnd") break;
      }

      scene->sky_texture_ =
          std::make_unique<ExrTexture>(sky_image_path);
      scene->sky_intensity_ = sky_intensity;
    }
  }

  scene->RecaluculateDirectionalShadowVolume();

  return scene;
}

Cameraクラスの変更

カメラの回転情報を取得できるようにします。

camera.h
  /**
   * @brief カメラの回転行列を取得する
   * @return カメラの回転行列
   */
  const glm::mat4 GetRotationMatrix() const;
camera.h
 private:
  GLfloat near_;
  GLfloat far_;
  glm::vec3 position_;
  glm::vec3 rotation_;
  glm::mat4 view_matrix_;
  glm::mat4 projection_matrix_;
  glm::mat4 rotation_matrix_;
camera.cpp
const glm::mat4 Camera::GetRotationMatrix() const { return rotation_matrix_; }
camera.cpp
void Camera::RecaluculateViewMatirx() {
  auto rotation = glm::eulerAngleYXZ(rotation_.y, rotation_.x, rotation_.z);
  rotation_matrix_ = rotation;  auto translate = glm::translate(glm::mat4(1), position_);
  view_matrix_ = glm::inverse(translate * rotation);
}

またいくつかのメンバ関数を実装します。

camera.h
  /**
   * @brief カメラのfovyを取得する
   * @return カメラのfovyの値
   */
  const GLfloat GetFovY() const;

  /**
   * @brief カメラのアスペクト比を取得する
   * @return カメラのアスペクト比の値
   */
  const GLfloat GetAspect() const;
camera.h
 private:
  GLfloat near_;
  GLfloat far_;
  GLfloat fovy_;  GLfloat aspect_;  glm::vec3 position_;
  glm::vec3 rotation_;
  glm::mat4 view_matrix_;
  glm::mat4 projection_matrix_;
  glm::mat4 rotation_matrix_;
camera.cpp
void Camera::Perspective(const GLfloat fovy, const GLfloat aspect,
                         const GLfloat near, const GLfloat far) {
  near_ = near;
  far_ = far;
  fovy_ = fovy;  aspect_ = aspect;  projection_matrix_ = glm::perspective(fovy, aspect, near, far);
}
camera.cpp
Camera::Camera(const glm::vec3 position, const glm::vec3 rotation,
               const GLfloat fovy, const GLfloat aspect, const GLfloat near,
               const GLfloat far)
    : near_(near),
      far_(far),
      fovy_(fovy),      aspect_(aspect),      position_(position),
      rotation_(rotation),
      view_matrix_(),
      projection_matrix_(glm::perspective(fovy, aspect, near, far)) {
  RecaluculateViewMatirx();
}
camera.cpp
const GLfloat Camera::GetFovY() const { return fovy_; }

const GLfloat Camera::GetAspect() const { return aspect_; }

SkyPassクラスの作成

SkyPassのクラスを作成します。

sky_pass.h
#ifndef OPENGL_PBR_MAP_SKY_PASS_H_
#define OPENGL_PBR_MAP_SKY_PASS_H_

#include <glm/ext.hpp>
#include <glm/glm.hpp>

#include "create_program.h"
#include "scene.h"

namespace game {

/**
 * @brief 空の描画のパスを表すクラス
 *
 * HDRカラーバッファのカラーを空の色で塗りつぶします。
 */
class SkyPass {
 public:
  /**
   * @brief このパスをレンダリングする
   * @param scene レンダリングするシーン
   */
  void Render(const Scene& sccene) const;

  /**
   * @brief コンストラクタ
   * @param hdr_color_fbo
   * @param fullscreen_mesh_vao 全画面を覆うメッシュのVAO
   * @param width ウィンドウの幅
   * @param height ウィンドウの高さ
   */
  SkyPass(const GLuint hdr_color_fbo, const GLuint fullscreen_mesh_vao,
          const GLuint width, const GLuint height);

  /**
   * @brief デストラクタ
   *
   * コンストラクタで確保したリソースを開放します。
   */
  ~SkyPass();

 private:
  const GLuint width_;
  const GLuint height_;

  const GLuint hdr_color_fbo_;

  const GLuint fullscreen_mesh_vao_;

  const GLuint shader_program_;
  const GLuint y_tan_loc_;
  const GLuint x_tan_loc_;
  const GLuint camera_rotation_matrix_loc_;
  const GLuint sky_intensity_loc_;
};

}  // namespace game

#endif  // OPENGL_PBR_MAP_SKY_PASS_H_
sky_pass.cpp
#include "sky_pass.h"

namespace game {

void SkyPass::Render(const Scene& scene) const {
  glUseProgram(shader_program_);
  glBindFramebuffer(GL_FRAMEBUFFER, hdr_color_fbo_);

  glClear(GL_COLOR_BUFFER_BIT);

  glDepthMask(GL_FALSE);
  glDisable(GL_DEPTH_TEST);

  glStencilFunc(GL_ALWAYS, 0, 0);
  glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);

  glDisable(GL_BLEND);

  const GLfloat y_tan = std::tan(scene.camera_->GetFovY() / 2.0f);
  const GLfloat x_tan =
      std::tan(scene.camera_->GetFovY() * scene.camera_->GetAspect() / 2.0f);
  const glm::mat4 camera_rotation_matrix = scene.camera_->GetRotationMatrix();

  glUniform1fv(y_tan_loc_, 1, &y_tan);
  glUniform1fv(x_tan_loc_, 1, &x_tan);
  glUniformMatrix4fv(camera_rotation_matrix_loc_, 1, GL_FALSE,
                     &camera_rotation_matrix[0][0]);
  glUniform1fv(sky_intensity_loc_, 1, &scene.sky_intensity_);

  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, scene.sky_texture_->GetTextureId());

  glBindVertexArray(fullscreen_mesh_vao_);
  glDrawArrays(GL_TRIANGLES, 0, 3);
}

SkyPass::SkyPass(const GLuint hdr_color_fbo, const GLuint fullscreen_mesh_vao,
                 const GLuint width, const GLuint height)
    : width_(width),
      height_(height),

      hdr_color_fbo_(hdr_color_fbo),

      fullscreen_mesh_vao_(fullscreen_mesh_vao),

      shader_program_(
          CreateProgram("shader/SkyPass.vert", "shader/SkyPass.frag")),
      y_tan_loc_(glGetUniformLocation(shader_program_, "yTan")),
      x_tan_loc_(glGetUniformLocation(shader_program_, "xTan")),
      camera_rotation_matrix_loc_(
          glGetUniformLocation(shader_program_, "CameraRotationMatrix")),
      sky_intensity_loc_(
          glGetUniformLocation(shader_program_, "skyIntensity")) {}

SkyPass::~SkyPass() { glDeleteProgram(shader_program_); }

}  // namespace game

SkyPass用のシェーダ作成

shader/SkyPass.vertshader/SkyPass.fragを作成します。

shader/SkyPass.vert
#version 460

layout (location = 0) in vec2 position;

uniform mat4 CameraRotationMatrix;
uniform float yTan;
uniform float xTan;

out vec3 direction;

void main()
{
  direction = (CameraRotationMatrix * vec4(xTan * position.x, yTan * position.y, -1.0, 0.0)).xyz;
  gl_Position = vec4(position, 0.0, 1.0);
}
shader/SkyPass.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;
}
// ###########################################################################


void main()
{
  vec3 dir = normalize(direction);

  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;

  outputColor = sRGBToACES(texture(SkyImage, texcoord).rgb) * skyIntensity;
}

カメラの回転情報と画面を覆うメッシュを利用して、Skyの色を取得するためのベクトルを用意します。 その3次元の方向ベクトルをもとに色をサンプリングしています。

参考: 床井研究室 - 魚眼レンズ画像の平面展開

DirectionalLightPassクラスの変更

DirectionalLightPass::Renderのはじめで行っている処理をSkyPassに移したため削除します。

directional_light_pass.cpp
  // Lighting Pass
  glUseProgram(shader_program_);
  glBindFramebuffer(GL_FRAMEBUFFER, hdr_fbo_);

  glViewport(0, 0, width_, height_);

  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_2D, shadow_map_);

-  glClear(GL_COLOR_BUFFER_BIT);

SceneRendererクラスの変更点

SkyPassを追加します。

scene_renderer.h
#include "directional_light_pass.h"
#include "exposure_pass.h"
#include "geometry_pass.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_;
  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),
      exposure_pass_(hdr_color_buffer_, exposured_fbo_, fullscreen_mesh_vao_,
                     width, height),
      tonemapping_pass_(exposured_color_buffer_, fullscreen_mesh_vao_, width,
                        height) {}
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);

  exposure_pass_.SetExposure(0.001f);
  exposure_pass_.Render();

  tonemapping_pass_.Render();
}

実行結果

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

2020 04 12 15 23 36

背景にSkyが描画されるようになりました。

カメラを回してみると次のようになります。

ただしくEquirectangularな空が描画できていそうです?

Blenderと比較すると次のとおりです。

2020 04 12 15 23 47

IBL(Image Based Lighting)を実装していないため、Blenderの画面に比べて猿たちが暗いです。 IBLは今後実装していく予定です。

プログラム全文

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

GitHub: MatchaChoco010/OpenGL-PBR-Map at v19