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

はじめに

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

この記事ではPBRの古典的DeferredシェーディングのDirectionalLightの影を実装するところまでを行います。

2020 04 09 18 02 14

2020/04/13 追記

std::numeric_limits<float>::lowest()とすべきところをstd::numeric_limits<float>::min()としていたので、記事の方を修正しました。

シャドウマッピング

影の描画にはシャドウマッピング法を使います。

ディレクショナルライトのShadowはシーン全体に描画されてほしいので、シャドウマップを描画する際のシャドウボリュームとしてシーン全体をカバーする必要があります。 そのためにシーン全体のAABBを求めてそこからシャドウボリュームを決定します。

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

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

Meshクラスの変更点

Meshの頂点全体を走査してAABBのXMAX、YMAX、ZMAX、XMIN、YMIN、ZMINを計算します。

まずはヘッダを追加します。

mesh.h#include
#include <GLFW/glfw3.h>

#include <fstream>
#include <glm/glm.hpp>
#include <iostream>
#include <limits>
#include <memory>
#include <sstream>
#include <string>
#include <vector>

それぞれの最大値最小値を保持するメンバ変数を用意します。

mesh.h
class Mesh {
 public:
  float x_max_;  float y_max_;  float z_max_;  float x_min_;  float y_min_;  float z_min_;

コンストラクタでverticesに対してループして計算をします。

mesh.cpp
Mesh::Mesh(const std::vector<glm::vec3>& vertices,
           const std::vector<glm::vec3>& normals,
           const std::vector<glm::vec2>& uvs)
    : size_(vertices.size()) {
  auto tangents = CalculateTangents(vertices, uvs);

  x_max_ = std::numeric_limits<float>::lowest();  y_max_ = std::numeric_limits<float>::lowest();  z_max_ = std::numeric_limits<float>::lowest();  x_min_ = std::numeric_limits<float>::max();  y_min_ = std::numeric_limits<float>::max();  z_min_ = std::numeric_limits<float>::max();  for (const auto& v : vertices) {    if (v.x > x_max_) x_max_ = v.x;    if (v.y > y_max_) y_max_ = v.y;    if (v.z > z_max_) z_max_ = v.z;    if (v.x < x_min_) x_min_ = v.x;    if (v.y < y_min_) y_min_ = v.y;    if (v.z < z_min_) z_min_ = v.z;  }
  ...

MeshEntityクラスの変更点

MeshにModel行列を掛けたあとのAABBを計算します。 MeshのAABBから作られる直方体の8頂点に対してModel行列を掛けて、そこからXMAX、YMAX、ZMAX、XMIN、YMIN、ZMINを計算します。

まずはそれぞれの軸の最大値最小値を保持するメンバ変数を用意します。

mesh_entity.h
class MeshEntity {
 public:
  const std::shared_ptr<const Mesh> mesh_;
  const std::shared_ptr<const Material> material_;
  float x_max_;
  float y_max_;
  float z_max_;
  float x_min_;
  float y_min_;
  float z_min_;

次に最大値最小値の計算を行うメンバ関数を用意します。 Model行列に変更があった際には再計算を行う必要があります。

mesh_entity.h
  /**
   * @brief AABBを再計算する
   */
  void RecaluculateAABB();
mesh_entity.cpp
void MeshEntity::RecaluculateAABB() {
  std::vector<glm::vec3> vertices = {
      glm::vec3(mesh_->x_min_, mesh_->y_min_, mesh_->z_min_),
      glm::vec3(mesh_->x_min_, mesh_->y_min_, mesh_->z_max_),
      glm::vec3(mesh_->x_min_, mesh_->y_max_, mesh_->z_min_),
      glm::vec3(mesh_->x_min_, mesh_->y_max_, mesh_->z_max_),
      glm::vec3(mesh_->x_max_, mesh_->y_min_, mesh_->z_min_),
      glm::vec3(mesh_->x_max_, mesh_->y_min_, mesh_->z_max_),
      glm::vec3(mesh_->x_max_, mesh_->y_max_, mesh_->z_min_),
      glm::vec3(mesh_->x_max_, mesh_->y_max_, mesh_->z_max_),
  };
  x_max_ = std::numeric_limits<float>::lowest();
  y_max_ = std::numeric_limits<float>::lowest();
  z_max_ = std::numeric_limits<float>::lowest();
  x_min_ = std::numeric_limits<float>::max();
  y_min_ = std::numeric_limits<float>::max();
  z_min_ = std::numeric_limits<float>::max();
  for (const auto& vertex : vertices) {
    auto vec = model_matrix_ * glm::vec4(vertex, 1.0f);
    auto v = glm::vec3(vec.x, vec.y, vec.z) / vec.w;
    if (v.x > x_max_) x_max_ = v.x;
    if (v.y > y_max_) y_max_ = v.y;
    if (v.z > z_max_) z_max_ = v.z;
    if (v.x < x_min_) x_min_ = v.x;
    if (v.y < y_min_) y_min_ = v.y;
    if (v.z < z_min_) z_min_ = v.z;
  }
}

各種メンバ関数やコンストラクタでModel行列が変更されたあとに呼び出します。

mesh_entity.cpp
const glm::vec3 MeshEntity::GetPosition() const { return position_; }

void MeshEntity::SetPosition(const glm::vec3 position) {
  position_ = position;
  RecaluculateModelMatrix();
  RecaluculateAABB();}

const glm::vec3 MeshEntity::GetRotation() const { return rotation_; }

void MeshEntity::SetRotation(const glm::vec3 rotation) {
  rotation_ = rotation;
  RecaluculateModelMatrix();
  RecaluculateAABB();}

const glm::vec3 MeshEntity::GetScale() const { return scale_; }

void MeshEntity::SetScale(const glm::vec3 scale) {
  scale_ = scale;
  RecaluculateModelMatrix();
  RecaluculateAABB();}

const glm::mat4 MeshEntity::GetModelMatrix() const { return model_matrix_; }

MeshEntity::MeshEntity(const std::shared_ptr<const Mesh> mesh,
                       const std::shared_ptr<const Material> material,
                       const glm::vec3 position, const glm::vec3 rotation,
                       const glm::vec3 scale)
    : mesh_(mesh),
      material_(material),
      position_(position),
      rotation_(rotation),
      scale_(scale),
      model_matrix_() {
  RecaluculateModelMatrix();
  RecaluculateAABB();}

DirectionalLightクラスの変更点

影を描画するのに必要な情報を保持するようにします。

まずは影描画用のLightViewProjection行列を保持します。

directional_light.h
 private:
  GLfloat intensity_;
  glm::vec3 direction_;
  glm::vec3 color_;
  glm::mat4 light_view_projection_matrix_;

影ボリュームを設定するメンバ関数を作成します。

directional_light.h
  /**
   * @brief 影ボリュームを設定する
   * @param legt 影ボリュームのleftの値
   * @param right 影ボリュームのrightの値
   * @param bottom 影ボリュームのbottomの値
   * @param top 影ボリュームのtopの値
   * @param near 影ボリュームのnearの値
   * @param far 影ボリュームのfarの値
   * @param light_view ライトのビュー行列
   */
  void SetShadowVolume(const GLfloat left, const GLfloat right,
                       const GLfloat bottom, const GLfloat top,
                       const GLfloat near, const GLfloat far,
                       const glm::mat4 light_view);
directional_light.cpp
void DirectionalLight::SetShadowVolume(const GLfloat left, const GLfloat right,
                                       const GLfloat bottom, const GLfloat top,
                                       const GLfloat near, const GLfloat far,
                                       const glm::mat4 light_view) {
  auto light_projection = glm::ortho(left, right, bottom, top, near, far);
  light_view_projection_matrix_ = light_projection * light_view;
}

次にLightViewProjection行列を取得するメンバ関数を作成します。

directional_light.h
  /**
   * @brief 影行列を取得する
   * @return 影行列
   */
  const glm::mat4 GetLightViewProjectionMatrix() const;
directional_light.cpp
const glm::mat4 DirectionalLight::GetLightViewProjectionMatrix() const {
  return light_view_projection_matrix_;
}

コンストラクタで適当にシャドウボリュームの初期値を決めます。

directional_light.cpp
DirectionalLight::DirectionalLight(const GLfloat intensity,
                                   const glm::vec3 direction,
                                   const glm::vec3 color)
    : intensity_(intensity),
      direction_(glm::normalize(direction)),
      color_(color) {
  SetShadowVolume(-10.0, 10.0, -10.0f, 10.0f, 0, 50.0f, glm::mat4());
}

Sceneクラスの変更点

SceneのAABBを再計算しDirectionalLightのシャドウボリュームを再設定するメンバ関数を用意します。

scene.h
#include <glm/glm.hpp>
#include <limits>#include <memory>
#include <unordered_map>
#include <vector>

#include "camera.h"
#include "directional_light.h"
#include "mesh.h"
#include "mesh_entity.h"
#include "point_light.h"
#include "spot_light.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_;

  /**   * @brief 影ボリュームを再計算する   */  void RecaluculateDirectionalShadowVolume();
  ...
scene.cpp
void Scene::RecaluculateDirectionalShadowVolume() {
  auto x_max = std::numeric_limits<float>::lowest();
  auto y_max = std::numeric_limits<float>::lowest();
  auto z_max = std::numeric_limits<float>::lowest();
  auto x_min = std::numeric_limits<float>::max();
  auto y_min = std::numeric_limits<float>::max();
  auto z_min = std::numeric_limits<float>::max();
  for (const auto& mesh_entity : mesh_entities_) {
    if (x_max < mesh_entity.x_max_) x_max = mesh_entity.x_max_;
    if (y_max < mesh_entity.y_max_) y_max = mesh_entity.y_max_;
    if (z_max < mesh_entity.z_max_) z_max = mesh_entity.z_max_;
    if (x_min > mesh_entity.x_min_) x_min = mesh_entity.x_min_;
    if (y_min > mesh_entity.y_min_) y_min = mesh_entity.y_min_;
    if (z_min > mesh_entity.z_min_) z_min = mesh_entity.z_min_;
  }

  auto dir = glm::normalize(directional_light_->GetDirection());
  auto rot = glm::toMat4(glm::rotation(dir, glm::vec3(0.0f, 0.0f, -1.0f)));

  auto x_max_rotated = std::numeric_limits<float>::lowest();
  auto y_max_rotated = std::numeric_limits<float>::lowest();
  auto z_max_rotated = std::numeric_limits<float>::lowest();
  auto x_min_rotated = std::numeric_limits<float>::max();
  auto y_min_rotated = std::numeric_limits<float>::max();
  auto z_min_rotated = std::numeric_limits<float>::max();
  std::vector<glm::vec4> vertices = {
      glm::vec4(x_min, y_min, z_min, 1.0), glm::vec4(x_min, y_min, z_max, 1.0),
      glm::vec4(x_min, y_max, z_min, 1.0), glm::vec4(x_min, y_max, z_max, 1.0),
      glm::vec4(x_max, y_min, z_min, 1.0), glm::vec4(x_max, y_min, z_max, 1.0),
      glm::vec4(x_max, y_max, z_min, 1.0), glm::vec4(x_max, y_max, z_max, 1.0),
  };
  for (const auto& vertex : vertices) {
    auto v = rot * vertex;
    if (v.x > x_max_rotated) x_max_rotated = v.x;
    if (v.y > y_max_rotated) y_max_rotated = v.y;
    if (v.z > z_max_rotated) z_max_rotated = v.z;
    if (v.x < x_min_rotated) x_min_rotated = v.x;
    if (v.y < y_min_rotated) y_min_rotated = v.y;
    if (v.z < z_min_rotated) z_min_rotated = v.z;
  }

  glm::mat4 translate = glm::translate(
      glm::mat4(1.0f),
      glm::vec3(-(x_max_rotated + x_min_rotated) / 2,
                -(y_max_rotated + y_min_rotated) / 2, -z_max_rotated));

  auto light_view = translate * rot;

  directional_light_->SetShadowVolume(
      -(x_max_rotated - x_min_rotated) / 2, (x_max_rotated - x_min_rotated) / 2,
      -(y_max_rotated - y_min_rotated) / 2, (y_max_rotated - y_min_rotated) / 2,
      0.0f, (z_max_rotated - z_min_rotated), light_view);
}

まずはMeshEntityを全部ループしてAABBを計算します。

  auto x_max = std::numeric_limits<float>::lowest();
  auto y_max = std::numeric_limits<float>::lowest();
  auto z_max = std::numeric_limits<float>::lowest();
  auto x_min = std::numeric_limits<float>::max();
  auto y_min = std::numeric_limits<float>::max();
  auto z_min = std::numeric_limits<float>::max();
  for (const auto& mesh_entity : mesh_entities_) {
    if (x_max < mesh_entity.x_max_) x_max = mesh_entity.x_max_;
    if (y_max < mesh_entity.y_max_) y_max = mesh_entity.y_max_;
    if (z_max < mesh_entity.z_max_) z_max = mesh_entity.z_max_;
    if (x_min > mesh_entity.x_min_) x_min = mesh_entity.x_min_;
    if (y_min > mesh_entity.y_min_) y_min = mesh_entity.y_min_;
    if (z_min > mesh_entity.z_min_) z_min = mesh_entity.z_min_;
  }

そのAABBをDirectionalLightのシャドウボリュームを計算していきます。

  auto dir = glm::normalize(directional_light_->GetDirection());
  auto rot = glm::toMat4(glm::rotation(dir, glm::vec3(0.0f, 0.0f, -1.0f)));

  auto x_max_rotated = std::numeric_limits<float>::lowest();
  auto y_max_rotated = std::numeric_limits<float>::lowest();
  auto z_max_rotated = std::numeric_limits<float>::lowest();
  auto x_min_rotated = std::numeric_limits<float>::max();
  auto y_min_rotated = std::numeric_limits<float>::max();
  auto z_min_rotated = std::numeric_limits<float>::max();
  std::vector<glm::vec4> vertices = {
      glm::vec4(x_min, y_min, z_min, 1.0), glm::vec4(x_min, y_min, z_max, 1.0),
      glm::vec4(x_min, y_max, z_min, 1.0), glm::vec4(x_min, y_max, z_max, 1.0),
      glm::vec4(x_max, y_min, z_min, 1.0), glm::vec4(x_max, y_min, z_max, 1.0),
      glm::vec4(x_max, y_max, z_min, 1.0), glm::vec4(x_max, y_max, z_max, 1.0),
  };
  for (const auto& vertex : vertices) {
    auto v = rot * vertex;
    if (v.x > x_max_rotated) x_max_rotated = v.x;
    if (v.y > y_max_rotated) y_max_rotated = v.y;
    if (v.z > z_max_rotated) z_max_rotated = v.z;
    if (v.x < x_min_rotated) x_min_rotated = v.x;
    if (v.y < y_min_rotated) y_min_rotated = v.y;
    if (v.z < z_min_rotated) z_min_rotated = v.z;
  }

  glm::mat4 translate = glm::translate(
      glm::mat4(1.0f),
      glm::vec3(-(x_max_rotated + x_min_rotated) / 2,
                -(y_max_rotated + y_min_rotated) / 2, -z_max_rotated));

  auto light_view = translate * rot;

  directional_light_->SetShadowVolume(
      -(x_max_rotated - x_min_rotated) / 2, (x_max_rotated - x_min_rotated) / 2,
      -(y_max_rotated - y_min_rotated) / 2, (y_max_rotated - y_min_rotated) / 2,
      0.0f, (z_max_rotated - z_min_rotated), light_view);

2次元の図で計算の過程を説明します。 シーン全体のAABBとDirectionalLightが次のようだったとします。

2020 04 09 15 21 22

Directional Lightの方向から-z方向への回転を計算します。

2020 04 09 15 22 12

その回転の回転をシーンのAABBにかけてやります。

2020 04 09 15 22 56

回転したAABBのAABBを計算します。

2020 04 09 15 23 23

回転したシーンのAABBのAABBを次のように原点位置に平行移動します。

2020 04 09 15 23 51

平行移動した後に、AABBの最大値と最小値を求めます。

2020 04 09 15 24 55

AABBの回転と平行移動成分がLightView行列になります。 回転と平行移動後のAABBの各値がLightProjection行列のorthoのパラメータtopやleftなどになります。

Scene::LoadSceneの最後でシャドウボリュームを計算します。

scene.cpp
  }

  scene->RecaluculateDirectionalShadowVolume();
  return scene;

DirectionalLightPassクラスの変更点

まずはいくつかのメンバ変数とシャドウマップ作成用のstaticメンバ関数を作成します。

directional_light_pass.h
 private:
  const GLuint width_;
  const GLuint height_;

  const GLuint shadow_map_size_;
  const GLuint hdr_fbo_;
  const GLuint gbuffer0_;
  const GLuint gbuffer1_;
  const GLuint gbuffer2_;
  const GLuint fullscreen_vao_;

  const GLuint shadow_map_;  const GLuint shadow_map_fbo_;  const GLuint shadow_pass_shader_program_;  const GLuint shadow_pass_model_view_projection_loc_;
  const GLuint shader_program_;
  const GLuint light_direction_loc_;
  const GLuint light_intensity_loc_;
  const GLuint light_color_loc_;
  const GLuint world_camera_pos_loc_;
  const GLuint view_projection_i_loc_;
  const GLuint projection_params_loc_;
  const GLuint light_view_projection_loc_;
  /**   * @brief シャドウマップテクスチャを作成する   * @return 生成したテクスチャのID   */  static const GLuint CreateShadowMap(const GLuint shadow_map_size);  /**   * @brief シャドウマップのFBOを生成する   * @param shadow_map シャドウマップテクスチャのID   * @return 生成したFBOのID   */  static const GLuint CreateShadowMapFbo(const GLuint shadow_map);};

作成したstaticメンバ関数の実装は次のとおりです。

directional_light_pass.cpp
const GLuint DirectionalLightPass::CreateShadowMap(
    const GLuint shadow_map_size) {
  GLuint shadow_map;
  glGenTextures(1, &shadow_map);
  glBindTexture(GL_TEXTURE_2D, shadow_map);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, shadow_map_size,
               shadow_map_size, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE,
                  GL_COMPARE_REF_TO_TEXTURE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glBindTexture(GL_TEXTURE_2D, 0);
  return shadow_map;
}

const GLuint DirectionalLightPass::CreateShadowMapFbo(const GLuint shadow_map) {
  GLuint shadow_map_fbo;
  glGenFramebuffers(1, &shadow_map_fbo);
  glBindFramebuffer(GL_FRAMEBUFFER, shadow_map_fbo);
  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D,
                         shadow_map, 0);
  glBindFramebuffer(GL_FRAMEBUFFER, 0);
  return shadow_map_fbo;
}

シャドウマップ用に次を指定しているのがポイントです。

  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE,
                  GL_COMPARE_REF_TO_TEXTURE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);

DirectionalLightPass::Releaseで確保したシャドウマップとシェーダプログラムを開放するようにします。

directional_light_pass.cpp
DirectionalLightPass::~DirectionalLightPass() {
  glDeleteFramebuffers(1, &shadow_map_fbo_);  glDeleteRenderbuffers(1, &shadow_map_);
  glDeleteProgram(shadow_pass_shader_program_);  glDeleteProgram(shader_program_);
}

コンストラクタを次のように変更します。

directional_light_pass.cpp
DirectionalLightPass::DirectionalLightPass(
    const GLuint hdr_fbo, const GLuint gbuffer0, const GLuint gbuffer1,
    const GLuint gbuffer2, const GLuint fullscreen_vao, const GLuint width,
    const GLuint height)
    : width_(width),
      height_(height),

      shadow_map_size_(4096),
      hdr_fbo_(hdr_fbo),
      gbuffer0_(gbuffer0),
      gbuffer1_(gbuffer1),
      gbuffer2_(gbuffer2),
      fullscreen_vao_(fullscreen_vao),

      shadow_map_(CreateShadowMap(shadow_map_size_)),      shadow_map_fbo_(CreateShadowMapFbo(shadow_map_)),
      shadow_pass_shader_program_(          CreateProgram("shader/DirectionalLightShadowPass.vert",                        "shader/DirectionalLightShadowPass.frag")),      shadow_pass_model_view_projection_loc_(glGetUniformLocation(          shadow_pass_shader_program_, "ModelViewProjection")),
      shader_program_(CreateProgram("shader/DirectionalLightPass.vert",
                                    "shader/DirectionalLightPass.frag")),
      light_direction_loc_(
          glGetUniformLocation(shader_program_, "lightDirection")),
      light_intensity_loc_(
          glGetUniformLocation(shader_program_, "lightIntensity")),
      light_color_loc_(glGetUniformLocation(shader_program_, "lightColor")),
      world_camera_pos_loc_(
          glGetUniformLocation(shader_program_, "worldCameraPos")),
      view_projection_i_loc_(
          glGetUniformLocation(shader_program_, "ViewProjectionI")),
      projection_params_loc_(
          glGetUniformLocation(shader_program_, "ProjectionParams")),
      light_view_projection_loc_(          glGetUniformLocation(shader_program_, "LightViewProjection")) {}

新しくシャドウマップ用のシェーダプログラムを追加しています。 シャドウマップのサイズはとりあえず4096固定で。

次にDirectionalLightPass::RenderにShadowPassを追加します。

directional_light_pass.cpp
void DirectionalLightPass::Render(const Scene& scene) const {
  // Shadow Pass
  glUseProgram(shadow_pass_shader_program_);
  glBindFramebuffer(GL_FRAMEBUFFER, shadow_map_fbo_);

  glPolygonOffset(2.0f, 5.0f);
  glEnable(GL_POLYGON_OFFSET_FILL);

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

  glDepthMask(GL_TRUE);
  glEnable(GL_DEPTH_TEST);

  glViewport(0, 0, shadow_map_size_, shadow_map_size_);

  glClear(GL_DEPTH_BUFFER_BIT);

  auto light_view_projection_matrix =
      scene.directional_light_->GetLightViewProjectionMatrix();

  for (const auto& mesh_entity : scene.mesh_entities_) {
    auto model_view_projection =
        light_view_projection_matrix * mesh_entity.GetModelMatrix();
    glUniformMatrix4fv(shadow_pass_model_view_projection_loc_, 1, GL_FALSE,
                       &model_view_projection[0][0]);
    mesh_entity.mesh_->Draw();
  }

  // Lighting Pass
  ...

オフセットに適当な値をセットして、デプスバッファをクリアした後、シーン中のすべてのモデルをレンダリングしています。

Lighting Passも修正します。

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

  glStencilFunc(GL_EQUAL, 128, 128);
  glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
  glStencilMask(0);

  glDepthMask(GL_FALSE);
  glDisable(GL_DEPTH_TEST);

  const auto direction = scene.directional_light_->GetDirection();
  const auto intensity = scene.directional_light_->GetIntensity();
  const auto color = scene.directional_light_->GetColor();
  const auto 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(light_direction_loc_, 1, &direction[0]);
  glUniform1fv(light_intensity_loc_, 1, &intensity);
  glUniform3fv(light_color_loc_, 1, &color[0]);
  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]);
  glUniformMatrix4fv(light_view_projection_loc_, 1, GL_FALSE,
                     &light_view_projection_matrix[0][0]);

  glBindVertexArray(fullscreen_vao_);
  glDrawArrays(GL_TRIANGLES, 0, 3);

  glEnable(GL_BLEND);
  glBlendEquation(GL_FUNC_ADD);
  glBlendFunc(GL_ONE, GL_ONE);

シェーダの作成と変更

新しくshader/DirectionalLightShadowPass.vertshader/DirectionalLightShadowPass.fragを作成します。

shader/DirectionalLightShadowPass.vert
#version 460

layout (location = 0) in vec4 position;

uniform mat4 ModelViewProjection;

void main()
{
  gl_Position = ModelViewProjection * position;
}
shader/DirectionalLightShadowPass.frag
#version 460

void main() {}

デプスの更新が目的なのでフラグメントシェーダは空っぽです。

次にshader/DirectionalLightPass.fragを変更します。

まずはUniform変数を追加します。

shader/DirectionalLightPass.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 sampler2DShadow ShadowMap;
uniform vec3 lightDirection;
uniform float lightIntensity; // lx
uniform vec3 lightColor;

uniform vec3 worldCameraPos;
uniform mat4 ViewProjectionI;
uniform vec2 ProjectionParams; // x: near, y: far

uniform mat4 LightViewProjection;

つぎに影の影響度を計算します。

shader/DirectionalLightPass.frag
// 3x3 PCF Shadow ############################################################
float getShadowAttenuation(vec3 worldPos)
{
  vec4 lightPos = LightViewProjection * vec4(worldPos, 1.0);
  vec2 uv = lightPos.xy * vec2(0.5) + vec2(0.5);
  float depthFromWorldPos = (lightPos.z / lightPos.w) * 0.5 + 0.5;

  ivec2 shadowMapSize = textureSize(ShadowMap, 0);
  vec2 offset = 1.0 / shadowMapSize.xy;

  float shadow = 0.0;
  for (int i = -1; i <= 1; i++)
  {
    for (int j = -1; j <= 1; j++)
    {
      vec3 UVC = vec3(uv + offset * vec2(i, j), depthFromWorldPos + 0.00001);
      shadow += texture(ShadowMap, UVC).x;
    }
  }
  return shadow / 9.0;
}
// ###########################################################################

最初にライトから見たフラグメントの座標を計算します。 そのxy座標をもとにシャドウマップのUVを計算します。 また、ライト座標からdepthの値も求めます。 このdepthがシャドウマップのdepthより深いときは影になっていることになります。 影をソフトにするために3x3 PCF計算を行っています。

mainではシャドウを計算し、照度に掛け合わせています。

shader/DirectionalLightPass.frag
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);

  float shadow = getShadowAttenuation(worldPos);
  vec3 V = normalize(worldCameraPos - worldPos);
  vec3 N = normalize(normal);
  vec3 L = normalize(-lightDirection);
  vec3 H = normalize(L + V);

  vec3 irradiance = lightIntensity * sRGBToACES(lightColor) * dot(N, L) * shadow;  vec3 radianceDirectionalLight = BRDF(albedo, metallic, roughness, N, V, L, H) * irradiance;

  outRadiance = emissive + radianceDirectionalLight;
}

実行結果

実行結果を次に示します。

2020 04 09 18 02 14

DirectionalLightの影がついているのがわかります。 その他のライトについてはまだ影がついていません。 シャドウマップの解像度がだいぶ高いので、PCFのソフト感はあまりありませんね。

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

2020 04 09 18 03 35

プログラム全文

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

GitHub: MatchaChoco010/OpenGL-PBR-Map at v16