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

はじめに

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

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

2020 04 10 12 26 10

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

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

SpotLightクラスの変更点

SpotLightクラスに影の情報をもたせます。

まずは影を描画する際のnearの値を操作する手段を用意します。

spot_light.h
    /**
   * @brief スポットライトの影のnearを取得する
   * @return スポットライトの影のnearの値
   */
  const GLfloat GetNear() const;

  /**
   * @brief スポットライトの影のnearの値を設定する
   * @param near スポットライトの影の新しいnearの値
   *
   * 内部で保持する影行列の再計算が行われます。
   */
  void SetNear(const GLfloat near);
spot_light.h
 private:
  glm::vec3 position_;
  GLfloat intensity_;
  glm::vec3 color_;
  GLfloat near_;  GLfloat range_;
  glm::vec3 direction_;
  GLfloat angle_;
  GLfloat blend_;
  glm::mat4 model_matrix_;

影行列も保持するようにします。

spot_light.h
 private:
  glm::vec3 position_;
  GLfloat intensity_;
  glm::vec3 color_;
  GLfloat near_;
  GLfloat range_;
  glm::vec3 direction_;
  GLfloat angle_;
  GLfloat blend_;
  glm::mat4 light_view_projection_matrix_;  glm::mat4 model_matrix_;

影行列計算用のメンバ関数も用意します。

spot_light.h
  /**
   * @brief 影行列を再計算する
   *
   * position_、direction_、range_、angle_から
   * light_view_projection_matrix_を再計算します。
   */
  void RecaluculateLightViewProjectionMatrix();

SpotLight::GetNearSpotLight::SetNearを実装します。

spot_light.cpp
void SpotLight::SetNear(const GLfloat near) {
  near_ = near;
  RecaluculateLightViewProjectionMatrix();
}

SpotLight::SetRangeRecaluculateLightViewProjectionMatrixの呼び出しを加えます。

spot_light.cpp
void SpotLight::SetRange(const GLfloat range) {
  range_ = range;
  RecaluculateModelMatrix();
  RecaluculateLightViewProjectionMatrix();
}

SpotLight::SetDirectionRecaluculateLightViewProjectionMatrixの呼び出しを加えます。

spot_light.cpp
void SpotLight::SetDirection(const glm::vec3 direction) {
  direction_ = glm::normalize(direction);
  RecaluculateLightViewProjectionMatrix();
}

SpotLight::SetAngleRecaluculateLightViewProjectionMatrixの呼び出しを加えます。

spot_light.cpp
void SpotLight::SetAngle(const GLfloat angle) {
  angle_ = angle;
  RecaluculateLightViewProjectionMatrix();
}

コンストラクタを書き換えます。

spot_light.cpp
SpotLight::SpotLight(const glm::vec3 position, const GLfloat intensity,
                     const glm::vec3 color, const GLfloat near, const GLfloat range,                     const glm::vec3 direction, const GLfloat angle,
                     const GLfloat blend)
    : position_(position),
      intensity_(intensity),
      color_(color),
      near_(near),      range_(range),
      direction_(glm::normalize(direction)),
      angle_(angle),
      blend_(blend) {
  RecaluculateModelMatrix();
  RecaluculateLightViewProjectionMatrix();}

RecaluculateLightViewProjectionMatrixの実装を書きます。

spot_light.cpp
void SpotLight::RecaluculateLightViewProjectionMatrix() {
  auto light_view =
      glm::lookAt(position_, position_ + direction_, glm::vec3(0, 1, 0));
  auto light_projection = glm::perspective(angle_, 1.0f, near_, range_);
  light_view_projection_matrix_ = light_projection * light_view;
}

影行列を取得するメンバ関数を用意します。

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

MeshEntityクラスの変更点

AABBと球体の当たり判定を計算するメンバ関数を用意します。

mesh_entity.h
  /**
   * @brief AABBと球体の当たり判定を計算する
   * @return hitしていればtrue、そうでなければfalse
   */
  bool TestSphereAABB(const glm::vec3 center, const GLfloat radius) const;
mesh_entity.cpp
bool MeshEntity::TestSphereAABB(const glm::vec3 center,
                                const GLfloat radius) const {
  auto closest_point = center;
  if (closest_point.x > x_max_) closest_point.x = x_max_;
  if (closest_point.x < x_min_) closest_point.x = x_min_;
  if (closest_point.y > y_max_) closest_point.y = y_max_;
  if (closest_point.y < y_min_) closest_point.y = y_min_;
  if (closest_point.z > z_max_) closest_point.z = z_max_;
  if (closest_point.z < z_min_) closest_point.z = z_min_;
  auto distance = glm::distance(closest_point, center);
  return distance <= radius;
}

球の中心から最も近いAABBの点を求めて、その距離とradiusで計算しています。

SpotLightPassクラスの変更点

SpotLightPassクラスにシャドウマップとシャドウパス用のメンバ変数をもたせます。

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

  const GLuint shadow_map_size_;
  const GLuint hdr_color_fbo_;
  const GLuint gbuffer0_;
  const GLuint gbuffer1_;
  const GLuint gbuffer2_;
  const GLuint sphere_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 stencil_pass_shader_program_;
  const GLuint stencil_pass_model_view_projection_loc_;

  const GLuint shader_program_;
  const GLuint model_view_projection_loc_;
  const GLuint world_light_position_loc_;
  const GLuint light_intensity_loc_;
  const GLuint light_color_loc_;
  const GLuint light_range_loc_;
  const GLuint light_direction_loc_;
  const GLuint light_angle_loc_;
  const GLuint light_blend_loc_;
  const GLuint world_camera_pos_loc_;
  const GLuint view_projection_i_loc_;
  const GLuint projection_params_loc_;
  const GLuint light_view_projection_loc_;

シャドウマップ作成用のstaticメンバ関数を作成します。

spot_light_pass.h
  /**
   * @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);
spot_light_pass.cpp
const GLuint SpotLightPass::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 SpotLightPass::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);

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

spot_light_pass.cpp
SpotLightPass::SpotLightPass(const GLuint hdr_color_fbo, const GLuint gbuffer0,
                             const GLuint gbuffer1, const GLuint gbuffer2,
                             const GLuint sphere_vao, const GLuint width,
                             const GLuint height)
    : width_(width),
      height_(height),

      shadow_map_size_(512),
      hdr_color_fbo_(hdr_color_fbo),
      gbuffer0_(gbuffer0),
      gbuffer1_(gbuffer1),
      gbuffer2_(gbuffer2),
      sphere_vao_(sphere_vao),

      shadow_map_(CreateShadowMap(shadow_map_size_)),      shadow_map_fbo_(CreateShadowMapFbo(shadow_map_)),
      shadow_pass_shader_program_(          CreateProgram("shader/SpotLightShadowPass.vert",                        "shader/SpotLightShadowPass.frag")),      shadow_pass_model_view_projection_loc_(glGetUniformLocation(          shadow_pass_shader_program_, "ModelViewProjection")),
      stencil_pass_shader_program_(
          CreateProgram("shader/PunctualLightStencilPass.vert",
                        "shader/PunctualLightStencilPass.frag")),
      stencil_pass_model_view_projection_loc_(glGetUniformLocation(
          stencil_pass_shader_program_, "ModelViewProjection")),

      shader_program_(CreateProgram("shader/SpotLightPass.vert",
                                    "shader/SpotLightPass.frag")),
      model_view_projection_loc_(
          glGetUniformLocation(shader_program_, "ModelViewProjection")),
      world_light_position_loc_(
          glGetUniformLocation(shader_program_, "worldLightPosition")),
      light_intensity_loc_(
          glGetUniformLocation(shader_program_, "lightIntensity")),
      light_color_loc_(glGetUniformLocation(shader_program_, "lightColor")),
      light_range_loc_(glGetUniformLocation(shader_program_, "lightRange")),
      light_direction_loc_(
          glGetUniformLocation(shader_program_, "lightDirection")),
      light_angle_loc_(glGetUniformLocation(shader_program_, "lightAngle")),
      light_blend_loc_(glGetUniformLocation(shader_program_, "lightBlend")),
      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")) {}

デストラクタで確保したシャドウマップを破棄するようにします。

spot_light_pass.cpp
SpotLightPass::~SpotLightPass() {
  glDeleteFramebuffers(1, &shadow_map_fbo_);  glDeleteTextures(1, &shadow_map_);
  glDeleteProgram(shadow_pass_shader_program_);  glDeleteProgram(stencil_pass_shader_program_);
  glDeleteProgram(shader_program_);
}

SpotLightPass::Renderにシャドウパスを追加します。

spot_light_pass.cpp
  for (const SpotLight& spot_light : scene.spot_lights_) {
    // Shadow Map Pass
    glUseProgram(shadow_pass_shader_program_);
    glBindFramebuffer(GL_FRAMEBUFFER, shadow_map_fbo_);

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

    glPolygonOffset(8.0f, 1.0f);
    glEnable(GL_POLYGON_OFFSET_FILL);

    glViewport(0, 0, shadow_map_size_, shadow_map_size_);

    glDepthMask(GL_TRUE);
    glEnable(GL_DEPTH_TEST);

    glClear(GL_DEPTH_BUFFER_BIT);

    const glm::mat4 light_view_projection =
        spot_light.GetLightViewProjectionMatrix();

    for (const auto& mesh_entity : scene.mesh_entities_) {
      if (!mesh_entity.TestSphereAABB(spot_light.GetPosition(),
                                     spot_light.GetRange())) {
        continue;
      }

      const glm::mat4 model_view_projection =
          light_view_projection * mesh_entity.GetModelMatrix();

      glUniformMatrix4fv(shadow_pass_model_view_projection_loc_, 1, GL_FALSE,
                         &model_view_projection[0][0]);
      mesh_entity.mesh_->Draw();
    }

適当なオフセットを与えてシャドウマップにデプスを描画をしていきます。

描画するオブジェクトは、球体との当たり判定をとって描画をスキップしています。 ライトの範囲内のものしかシャドウには関係ないのでスキップしています。 コーンとの当たり判定を取ればきちんとスキップ可能ですが、ここではかんたんのため球体との当たり判定で計算しています。

ライトパスも変更します。

spot_light_pass.cpp
    // Lighting Pass
    glUseProgram(shader_program_);

    glDisable(GL_DEPTH_TEST);

    glStencilFunc(GL_NOTEQUAL, 0, 255);
    glStencilMask(0);
    glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);

    glEnable(GL_CULL_FACE);
    glCullFace(GL_FRONT);

    glDrawBuffer(GL_COLOR_ATTACHMENT0);
    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
    glDepthMask(GL_FALSE);

    const glm::vec3 world_light_pos = spot_light.GetPosition();
    const GLfloat light_intensity = spot_light.GetIntensity();
    const glm::vec3 light_color = spot_light.GetColor();
    const GLfloat light_range = spot_light.GetRange();
    const glm::vec3 light_direction = spot_light.GetDirection();
    const GLfloat light_angle = spot_light.GetAngle();
    const GLfloat light_blend = spot_light.GetBlend();
    const glm::vec3 world_camera_pos = scene.camera_->GetPosition();
    const glm::mat4 view_projection_i =
        glm::inverse(scene.camera_->GetViewProjectionMatrix());
    const glm::vec2 projection_params =
        glm::vec2(scene.camera_->GetNear(), scene.camera_->GetFar());

    glUniformMatrix4fv(model_view_projection_loc_, 1, GL_FALSE,
                       &model_view_projection[0][0]);
    glUniform3fv(world_light_position_loc_, 1, &world_light_pos[0]);
    glUniform1fv(light_intensity_loc_, 1, &light_intensity);
    glUniform3fv(light_color_loc_, 1, &light_color[0]);
    glUniform1fv(light_range_loc_, 1, &light_range);
    glUniform3fv(light_direction_loc_, 1, &light_direction[0]);
    glUniform1fv(light_angle_loc_, 1, &light_angle);
    glUniform1fv(light_blend_loc_, 1, &light_blend);
    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[0][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);
    glActiveTexture(GL_TEXTURE3);    glBindTexture(GL_TEXTURE_2D, shadow_map_);
    glBindVertexArray(sphere_vao_);
    glDrawElements(GL_TRIANGLES, 8 * 8 * 6, GL_UNSIGNED_INT, 0);

    glCullFace(GL_BACK);
  }

シェーダの作成と変更

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

shader/SpotLightShadowPass.vert
#version 460

layout (location = 0) in vec4 position;

uniform mat4 ModelViewProjection;

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

void main() {}

shader/SpotLightPass.fragを編集します。

まずはUniform変数の追加。

shader/SpotLightPass.frag
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 worldLightPosition;
uniform float lightIntensity; // lm
uniform vec3 lightColor;
uniform float lightRange;
uniform vec3 lightDirection;
uniform float lightAngle; // radian
uniform float lightBlend; // 0-1

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

uniform mat4 LightViewProjection;

次にシャドウを計算する関数を作成します。

shader/SpotLightPass.frag
// 3x3 PCF Shadow ##############################################################
float getShadowAttenuation(vec3 worldPos)
{
  vec4 lightPos = LightViewProjection * vec4(worldPos, 1.0);
  vec2 uv = lightPos.xy / lightPos.w * 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;
}
// #############################################################################

影をソフトにするために3x3のPCFを実装しています。

main関数でシャドウをirradianceに掛け合わせます。

shader/SpotLightPass.frag
void main()
{
  vec2 uv = CalcTexCoord();

  vec4 gbuffer0 = texture(GBuffer0, uv);
  vec4 gbuffer1 = texture(GBuffer1, uv);
  vec4 gbuffer2 = texture(GBuffer2, uv);

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

  float shadow = getShadowAttenuation(worldPos);

  vec3 V = normalize(worldCameraPos - worldPos);
  vec3 N = normalize(normal);
  vec3 L = normalize(worldLightPosition - worldPos);
  vec3 H = normalize(L + V);

  float distance = length(worldLightPosition - worldPos);
  vec3 irradiance = LightIrradiance(lightIntensity, sRGBToACES(lightColor), L, N, distance) * shadow;

  outRadiance = BRDF(albedo, metallic, roughness, N, V, L, H) * irradiance;
}

Sceneクラスの変更

Scene::LoadSceneのスポットライトの読み込み部分を変更しnearを追加します。

scene.cpp
    // Spot Light
    else if (texts[0] == "SpotLight:") {
      glm::vec3 position;
      GLfloat intensity;
      glm::vec3 color;
      GLfloat near;
      GLfloat range;
      glm::vec3 direction;
      GLfloat angle;
      GLfloat blend;

      while (std::getline(ifs, line)) {
        auto texts = SplitString(line, ' ');

        if (texts[0] == "Position:") {
          position = glm::vec3(std::stof(texts[1]), std::stof(texts[3]),
                               -std::stof(texts[2]));
        } else if (texts[0] == "Intensity:") {
          intensity = std::stof(texts[1]);
        } else if (texts[0] == "Color:") {
          color = glm::vec3(std::stof(texts[1]), std::stof(texts[2]),
                            std::stof(texts[3]));
        } else if (texts[0] == "ClipStart:") {
          near = std::stof(texts[1]);
        } else if (texts[0] == "Range:") {
          range = std::stof(texts[1]);
        } else if (texts[0] == "Direction:") {
          direction = glm::vec3(std::stof(texts[1]), std::stof(texts[3]),
                                -std::stof(texts[2]));
        } else if (texts[0] == "Angle:") {
          angle = std::stof(texts[1]);
        } else if (texts[0] == "Blend:") {
          blend = std::stof(texts[1]);
        }

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

      scene->spot_lights_.emplace_back(position, intensity, color, near, range,
                                       direction, angle, blend);
    }

実行結果

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

2020 04 10 12 26 10

前回と比べるとピンク色のスポットライトの光に影が加わったのがわかります。 まだ緑や水色のポイントライトには影はありません。

2020 04 10 12 26 41

プログラム全文

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

GitHub: MatchaChoco010/OpenGL-PBR-Map at v17