OpenGLでDeferredシェーディングを実装する(SkyPass)
はじめに
この記事はシリーズ記事です。目次はこちら。
この記事ではPBRの古典的DeferredシェーディングでSkyを描画してみます。
Sky
前回まではオブジェクトが描画されない部分は画面クリアした黒で描画されたままになっていました。 今回はその部分に空を描画してみます。
Blenderアドオンの変更点
Blendファイルに空を追加します。
BackgroundにEnvirounmentなImageを追加します。 Equirectangularな画像を追加しました。 これは縦横1:2のHDRi画像を設定するものです。
今回は次のような.exrファイルを用意しました。 .exrというのはHDRを扱える画像形式の一つです。
この画像はKritaというペイントソフトで作成したものです。 KritaにはHDRな画像をペイントする機能があるので今回はKritaを使いましたが、好きなペイントソフトを使えば良いと思います。
Blendファイルに設定してみると次のとおりです。
環境光成分に環境テクスチャが使われるようで、、シーン全体が明るくなりました。
このSkyの画像をBlendファイルからSceneFileに書き出すようにアドオンを変更します。
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ファイルを読み込み保持するクラスを作成します。
#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_
#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テクスチャを保持するように変更します。
#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テクスチャを読み込むようにします。
...
// 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クラスの変更
カメラの回転情報を取得できるようにします。
/**
* @brief カメラの回転行列を取得する
* @return カメラの回転行列
*/
const glm::mat4 GetRotationMatrix() const;
private:
GLfloat near_;
GLfloat far_;
glm::vec3 position_;
glm::vec3 rotation_;
glm::mat4 view_matrix_;
glm::mat4 projection_matrix_;
glm::mat4 rotation_matrix_;
const glm::mat4 Camera::GetRotationMatrix() const { return rotation_matrix_; }
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);
}
またいくつかのメンバ関数を実装します。
/**
* @brief カメラのfovyを取得する
* @return カメラのfovyの値
*/
const GLfloat GetFovY() const;
/**
* @brief カメラのアスペクト比を取得する
* @return カメラのアスペクト比の値
*/
const GLfloat GetAspect() const;
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_;
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::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();
}
const GLfloat Camera::GetFovY() const { return fovy_; }
const GLfloat Camera::GetAspect() const { return aspect_; }
SkyPassクラスの作成
SkyPassのクラスを作成します。
#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_
#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.vert
とshader/SkyPass.frag
を作成します。
#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);
}
#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に移したため削除します。
// 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を追加します。
#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"
GeometryPass geometry_pass_;
SkyPass sky_pass_; DirectionalLightPass directional_light_pass_;
PointLightPass point_light_pass_;
SpotLightPass spot_light_pass_;
ExposurePass exposure_pass_;
TonemappingPass tonemapping_pass_;
SceneRenderer::SceneRenderer(const GLuint width, const GLuint height)
: width_(width),
height_(height),
fullscreen_mesh_vao_(CreateFullscreenMeshVao()),
fullscreen_mesh_vertices_vbo_(
CreateFullscreenMeshVerticesVbo(fullscreen_mesh_vao_)),
fullscreen_mesh_uvs_vbo_(
CreateFullscreenMeshUvsVbo(fullscreen_mesh_vao_)),
sphere_vao_(CreateSphereMeshVao()),
sphere_vertices_vbo_(CreateSphereMeshVbo(sphere_vao_)),
sphere_indices_ibo_(CreateSphereMeshIbo(sphere_vao_)),
gbuffer0_(CreateGBuffer0(width, height)),
gbuffer1_(CreateGBuffer1(width, height)),
gbuffer2_(CreateGBuffer2(width, height)),
gbuffer_depth_(CreateGBufferDepth(width, height)),
gbuffer_fbo_(
CreateGBufferFbo(gbuffer0_, gbuffer1_, gbuffer2_, gbuffer_depth_)),
hdr_color_buffer_(CreateHdrColorBuffer(width, height)),
hdr_depth_buffer_(CreateHdrDepthBuffer(width, height)),
hdr_fbo_(CreateHdrFbo(hdr_color_buffer_, hdr_depth_buffer_)),
exposured_color_buffer_(CreateExposuredColorBuffer(width, height)),
exposured_fbo_(CreateExposuredFbo(exposured_color_buffer_)),
geometry_pass_(gbuffer_fbo_),
sky_pass_(hdr_fbo_, fullscreen_mesh_vao_, width_, height_), directional_light_pass_(hdr_fbo_, gbuffer0_, gbuffer1_, gbuffer2_,
fullscreen_mesh_vao_, width, height),
point_light_pass_(hdr_fbo_, gbuffer0_, gbuffer1_, gbuffer2_, sphere_vao_,
width, height),
spot_light_pass_(hdr_fbo_, gbuffer0_, gbuffer1_, gbuffer2_, sphere_vao_,
width, height),
exposure_pass_(hdr_color_buffer_, exposured_fbo_, fullscreen_mesh_vao_,
width, height),
tonemapping_pass_(exposured_color_buffer_, fullscreen_mesh_vao_, width,
height) {}
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();
}
実行結果
実行結果は次のとおりです。
背景にSkyが描画されるようになりました。
カメラを回してみると次のようになります。
ただしくEquirectangularな空が描画できていそうです?
Blenderと比較すると次のとおりです。
IBL(Image Based Lighting)を実装していないため、Blenderの画面に比べて猿たちが暗いです。 IBLは今後実装していく予定です。
プログラム全文
プログラム全文はGitHubにアップロードしてあります。