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

はじめに

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

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

2020 04 07 18 05 04

スポットライトのライティング

ポイントライトと殆ど変わりません。 今回は簡単のためスポットライトであっても球体メッシュを使ってポイントライトと同様にしてライティングすべき領域を計算します。 ライティングの計算の際にスポットライトの照らしている範囲外は0になるので余計な計算は増えますが、最終的な絵に影響はありません。 球状のメッシュではなくコーンのような形を使うとより良いのかもしれませんが……。

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

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

SpotLightクラスの作成

まずSpotLightクラスを作成します。

spot_light.h
#ifndef OPENGL_PBR_MAP_SPOT_LIGHT_H_
#define OPENGL_PBR_MAP_SPOT_LIGHT_H_

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

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

namespace game {

/**
 * @brief スポットライトを表現するオブジェクト
 */
class SpotLight {
 public:
  /**
   * @brief スポットライトの位置を取得する
   * @return スポットライトの位置
   */
  const glm::vec3 GetPosition() const;

  /**
   * @brief スポットライトの位置を設定する
   * @param position スポットライトの新しい位置
   *
   * 内部で保持する影行列とModel行列の再計算が行われます。
   */
  void SetPosition(const glm::vec3 position);

  /**
   * @brief スポットライトの強さを取得する
   * @return スポットライトの強さ(lm)
   *
   * スポットライトの強さはlm(ルーメン)で表現します。
   */
  const GLfloat GetIntensity() const;

  /**
   * @brief スポットライトの強さを設定する
   * @param intensity スポットライトの新しい強さ
   *
   * スポットライトの強さはlm(ルーメン)で表現します。
   */
  void SetIntensity(const GLfloat intensity);

  /**
   * @brief スポットライトの色を取得する
   * @return スポットライトの色
   */
  const glm::vec3 GetColor() const;

  /**
   * @brief スポットライトの色を設定する
   * @param color スポットライトの新しい色
   */
  void SetColor(const glm::vec3 color);

  /**
   * @brief スポットライトのRangeを取得する
   * @return スポットライトのRange(m)
   */
  const GLfloat GetRange() const;

  /**
   * @brief スポットライトのRangeを設定する
   * @param range スポットライトの新しいRange(m)
   *
   * 内部で保持する影行列とModel行列の再計算が行われます。
   */
  void SetRange(const GLfloat range);

  /**
   * @brief スポットライトの向きを取得する
   * @return スポットライトの向きのベクトル
   *
   * 正規化された向きベクトルを返します。
   */
  const glm::vec3 GetDirection() const;

  /**
   * @brief スポットライトの向きを設定する
   * @param direction 新しいスポットライトの向きのベクトル
   *
   * 渡されたベクトルは正規化され保持されます。
   * 内部で保持する影行列を再計算します。
   */
  void SetDirection(const glm::vec3 direction);

  /**
   * @brief スポットライトのAngleを取得する
   * return スポットライトのAngle(radians)
   */
  const GLfloat GetAngle() const;

  /**
   * @brief スポットライトのAngleを設定する
   * @param angle スポットライトのAngle(radians)
   *
   * 内部で保持する影行列を再計算します。
   */
  void SetAngle(const GLfloat angle);

  /**
   * @brief スポットライトの半影の割合を取得する
   * @return スポットライトの半影の割合([0,1])
   *
   * スポットライトの半影と本影のブレンド率を取得します。
   * 0で本影のみ、1で半影のみとなります。
   */
  const GLfloat GetBlend() const;

  /**
   * @brief スポットライトの半影の割合を設定する
   * @param blend スポットライトの半影の割合([0,1])
   *
   * スポットライトの半影と本影のブレンド率を取得します。
   * 0で本影のみ、1で半影のみとなります。
   */
  void SetBlend(const GLfloat blend);

  /**
   * @brief Model行列を取得する
   * @return Model行列
   *
   * range_ + 0.1fの拡大も含みます。
   */
  const glm::mat4 GetModelMatrix() const;

  /**
   * @brief コンストラクタ
   * @param position スポットライトの位置
   * @param intensity スポットライトの強さ(lm)
   * @param color スポットライトの色
   * @param range スポットライトのRangeの値
   * @param direction スポットライトの向きのベクトル
   * @param angle スポットライトのAngle(radians)
   * @param blend スポットライトの半影の割合([0,1])
   */
  SpotLight(const glm::vec3 position, const GLfloat intensity,
            const glm::vec3 color, const GLfloat range,
            const glm::vec3 direction, const GLfloat angle,
            const GLfloat blend);

 private:
  glm::vec3 position_;
  GLfloat intensity_;
  glm::vec3 color_;
  GLfloat range_;
  glm::vec3 direction_;
  GLfloat angle_;
  GLfloat blend_;
  glm::mat4 model_matrix_;

  /**
   * @brief Model行列を再計算する
   *
   * position_からmodel_matrix_を再計算します。
   */
  void RecaluculateModelMatrix();
};

}  // namespace game

#endif  // OPENGL_PBR_MAP_SPOT_LIGHT_H_
spot_light.cpp
#include "spot_light.h"

namespace game {

const glm::vec3 SpotLight::GetPosition() const { return position_; }

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

const GLfloat SpotLight::GetIntensity() const { return intensity_; }

void SpotLight::SetIntensity(const GLfloat intensity) {
  intensity_ = intensity;
}

const glm::vec3 SpotLight::GetColor() const { return color_; }

void SpotLight::SetColor(const glm::vec3 color) { color_ = color; }

const GLfloat SpotLight::GetRange() const { return range_; }

void SpotLight::SetRange(const GLfloat range) {
  range_ = range;
  RecaluculateModelMatrix();
}

const glm::vec3 SpotLight::GetDirection() const { return direction_; }

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

const GLfloat SpotLight::GetAngle() const { return angle_; }

void SpotLight::SetAngle(const GLfloat angle) { angle_ = angle; }

const GLfloat SpotLight::GetBlend() const { return blend_; }

void SpotLight::SetBlend(const GLfloat blend) { blend_ = blend; }

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

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

void SpotLight::RecaluculateModelMatrix() {
  model_matrix_ = glm::translate(glm::mat4(1), position_) *
                  glm::scale(glm::mat4(1), glm::vec3(range_ + 0.1f));
}

}  // namespace game

スポットライトのプロパティを保持するクラスです。 スポットライトの影の外側と内側の割合を0-1のblendというパラメータで表現しています。

Sceneクラスの変更

scene.h
#ifndef OPENGL_PBR_MAP_SCENE_H_
#define OPENGL_PBR_MAP_SCENE_H_

#include <glm/glm.hpp>
#include <memory>
#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 テスト用のシーンを作成する
   * @param width ウィンドウの幅
   * @param height ウィンドウの高さ
   * @return テスト用シーン
   */
  static std::unique_ptr<Scene> CreateTestScene(const int width,
                                                const int height);
};

}  // namespace game

#endif  // OPENGL_PBR_MAP_SCENE_H_
scene.cpp
  // PointLightの追加
  scene->point_lights_.emplace_back(glm::vec3(-2.0f, 0.0f, 2.0f), 600.0f,
                                    glm::vec3(0.5f, 0.8f, 1.0f), 10.0f);
  scene->point_lights_.emplace_back(glm::vec3(2.0f, 0.0f, 2.0f), 600.0f,
                                    glm::vec3(0.5f, 1.0f, 0.8f), 10.0f);

  // SpotLightの追加  scene->spot_lights_.emplace_back(      glm::vec3(0.0f, 5.0f, 5.0f), 2200.0f, glm::vec3(1.0f, 0.5f, 0.5f), 20.0f,      glm::vec3(0.0f, -1.0f, -1.0f), glm::radians(30.0f), 0.2);

スポットライトもポイントライトと同様にvectorでもたせます。 今回のテスト用シーンには一つだけ追加しました。

SpotLightPassクラスの作成

次にSpotLightPassクラスを作成します。

spot_light_pass.h
#ifndef OPENGL_PBR_MAP_POINT_LIGHT_PASS_H_
#define OPENGL_PBR_MAP_POINT_LIGHT_PASS_H_

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

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

namespace game {

/**
 * @brief PointLighatパスを表現するクラス
 *
 * GBufferとシーンのPointLightの情報をもとに
 * PointLightのライティングを計算しHDRのカラーバッファに書き込みます。
 */
class PointLightPass {
 public:
  /**
   * @brief このパスをレンダリングする
   * @param scene レンダリングするシーン
   *
   * PointLightのライティングをHDRカラーバッファに書き出します。
   */
  void Render(const Scene& scene) const;

  /**
   * @brief コンストラクタ
   * @param hdr_color_fbo HDRのフレームバッファオブジェクト
   * @param gbuffer0 GBuffer0のテクスチャID
   * @param gbuffer1 GBuffer1のテクスチャID
   * @param gbuffer2 GBuffer2のテクスチャID
   * @param sphere_vao 球のMeshのVAO
   * @param width ウィンドウの幅
   * @param height ウィンドウの高さ
   */
  PointLightPass(const GLuint hdr_color_fbo, const GLuint gbuffer0,
                 const GLuint gbuffer1, const GLuint gbuffer2,
                 const GLuint sphere_vao, const GLuint width,
                 const GLuint height);

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

 private:
  const GLuint width_;
  const GLuint height_;

  const GLuint hdr_fbo_;
  const GLuint gbuffer0_;
  const GLuint gbuffer1_;
  const GLuint gbuffer2_;
  const GLuint sphere_vao_;

  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 world_camera_pos_loc_;
  const GLuint view_projection_i_loc_;
  const GLuint projection_params_loc_;
};

}  // namespace game

#endif  // OPENGL_PBR_MAP_POINT_LIGHT_PASS_H_
spot_light_pass.cpp
#include "spot_light_pass.h"

namespace game {

void SpotLightPass::Render(const Scene& scene) const {
  for (const SpotLight& spot_light : scene.spot_lights_) {
    // Stencil Pass
    glUseProgram(stencil_pass_shader_program_);
    glBindFramebuffer(GL_FRAMEBUFFER, hdr_color_fbo_);

    glViewport(0, 0, width_, height_);

    glDisable(GL_CULL_FACE);

    glEnable(GL_DEPTH_TEST);

    glStencilMask(255);
    glClear(GL_STENCIL_BUFFER_BIT);

    glStencilFunc(GL_ALWAYS, 0, 0);
    glStencilOpSeparate(GL_BACK, GL_KEEP, GL_INCR_WRAP, GL_KEEP);
    glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_DECR_WRAP, GL_KEEP);

    glDrawBuffer(GL_NONE);
    glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
    glDepthMask(GL_FALSE);

    glm::mat4 model_view_projection =
        scene.camera_->GetViewProjectionMatrix() * spot_light.GetModelMatrix();
    glUniformMatrix4fv(stencil_pass_model_view_projection_loc_, 1, GL_FALSE,
                       &model_view_projection[0][0]);

    glBindVertexArray(sphere_vao_);
    glDrawElements(GL_TRIANGLES, 8 * 8 * 6, GL_UNSIGNED_INT, 0);

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

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

    glBindVertexArray(sphere_vao_);
    glDrawElements(GL_TRIANGLES, 8 * 8 * 6, GL_UNSIGNED_INT, 0);

    glCullFace(GL_BACK);
  }
}

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),
      hdr_color_fbo_(hdr_color_fbo),
      gbuffer0_(gbuffer0),
      gbuffer1_(gbuffer1),
      gbuffer2_(gbuffer2),
      sphere_vao_(sphere_vao),
      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")) {}

SpotLightPass::~SpotLightPass() {
  glDeleteProgram(stencil_pass_shader_program_);
  glDeleteProgram(shader_program_);
}

}  // namespace game

コンストラクタでは例によってG-Bufferと書き込み先のFBO、球体のメッシュを保持し、ShaderProgramを作成してUniformのLocationを取得しています。 ShaderProgramはステンシル用とライティング用で2つ用意しています。

SpotLightPass::Renderの処理の内部はほとんどポイントライトと同じです。 最初にステンシルパスを計算し、その次にライティングパスを計算します。 ライティングパスで渡すUniform変数が多少増えている程度ですね。

SpotLightPass用シェーダの作成

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

shader/SpotLightPass.vert
#version 460

layout (location = 0) in vec4 position;

uniform mat4 ModelViewProjection;

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

layout (location = 0) out vec3 outRadiance;

layout (binding = 0) uniform sampler2D GBuffer0;
layout (binding = 1) uniform sampler2D GBuffer1;
layout (binding = 2) uniform sampler2D GBuffer2;

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


const float PI = 3.14159265358979323846;


// Calc gbuffer texcoord #######################################################
vec2 CalcTexCoord()
{
   return gl_FragCoord.xy / textureSize(GBuffer0, 0);
}
// #############################################################################


// world pos from depth texture ################################################
float DecodeDepth(float d)
{
  return -d * (ProjectionParams.y - ProjectionParams.x) - ProjectionParams.x;
}

vec3 worldPosFromDepth(float d, vec2 uv)
{
  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(uv.x * 2.0 - 1.0, uv.y * 2.0 - 1.0, z / w, 1.0);
  vec4 worldPos = ViewProjectionI * projectedPos;
  return worldPos.xyz / worldPos.w;
}
// #############################################################################


// attenuation #################################################################
float DistanceAttenuation(float distance)
{
  float EPSILON = 0.01;
  float att = 1.0 / (distance * distance + EPSILON);
  float smoothatt = 1 - pow(distance / lightRange, 4.0);
  smoothatt = max(smoothatt, 0.0);
  smoothatt =  smoothatt * smoothatt;
  return att * smoothatt;
}

float AngleAttenuation(vec3 L)
{
  float outerTheta = lightAngle / 2.0;
  float innerTheta = outerTheta * (1.0 - lightBlend);
  float cos_s = dot(-L, normalize(lightDirection));
  float cos_u = cos(outerTheta);
  float cos_p = cos(innerTheta);
  float t = (cos_s - cos_u) / (cos_p - cos_u);
  t = clamp(t, 0, 1);
  return t * t;
}

vec3 LightIrradiance(float intensity, vec3 color, vec3 L, vec3 N, float distance)
{
  return 1.0 / PI * intensity * color * max(0, dot(L, N)) * DistanceAttenuation(distance) * AngleAttenuation(L);
}
// #############################################################################


// BRDF ######################################################################
vec3 NormalizedLambert(vec3 diffuseColor) {
  return diffuseColor / PI;
}

vec3 F_Schlick(vec3 F0, vec3 H, vec3 V) {
  return (F0 + (1.0 - F0) * pow(1.0 - max(dot(V, H), 0), 5.0));
}

float D_GGX(float a, float NoH) {
  float a2 = a * a;
  float NoH2 = NoH * NoH;
  float d = NoH2 * (a2 - 1.0) + 1.0;
  return a2 / (PI * d * d);
}

float G_SchlickGGX(float a, float NoV) {
  float EPSILON = 0.001;
  float k = a * a / 2 + EPSILON;
  float up = NoV;
  float down = NoV * (1 - k) + k;
  return up / down;
}

float G_Smith(float a, float NoV, float NoL) {
  float ggx0 = G_SchlickGGX(1, NoV);
  float ggx1 = G_SchlickGGX(1, NoL);
  return ggx0 * ggx1;
}

vec3 BRDF(vec3 albedo, float metallic, float roughness, vec3 N, vec3 V, vec3 L, vec3 H) {
  float EPSILON = 0.001;
  vec3 F0 = mix(vec3(0.04), albedo, metallic);
  float NoH = max(dot(N, H), 0);
  float NoV = max(dot(N, V), 0);
  float NoL = max(dot(N, L), 0);
  float a = roughness * roughness;

  // specular
  vec3 F = F_Schlick(F0, H, V);
  float D = D_GGX(a, NoH);
  float G = G_Smith(a, NoV, NoL);
  vec3 up = F * D * G;
  float down = max(4.0 * NoV * NoL, EPSILON);
  vec3 specular = up / down;

  // diffuse
  vec3 diffuse = NormalizedLambert(albedo);

  vec3 kD = vec3(1.0) - F;
  kD *= 1.0 - metallic;
  return kD * diffuse + specular;
}
// ###########################################################################


// 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()
{
  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);

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

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

ポイントライトとほとんど同じ流れですが、減衰の計算の際にAngleによる減衰を追加しています。 Angle減衰はFrostbiteゲームエンジンの式に準拠しています。

14π\frac{1}{4 \pi}ではなく1π\frac{1}{\pi}となっているのもFrostbiteの式に合わせています。 本来であればワット数が同じスポットライトでAngleによって放射する方向が狭くなったらその分明るくなるはずですが、そのような計算を入れるとアーティストにとって使いにくいものになるため一様にπ\piで割る形になっているそうです。

SceneRendererクラスの変更点

SceneRendererクラスを変更します。

最初に読み込むヘッダを追加します。

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

#include <array>

#include "directional_light_pass.h"
#include "exposure_pass.h"
#include "geometry_pass.h"
#include "point_light_pass.h"
#include "scene.h"
#include "spot_light_pass.h"#include "tonemapping_pass.h"

SpotLightPassをメンバ変数として追加します。

scene_renderer.h
  GeometryPass geometry_pass_;
  DirectionalLightPass directional_light_pass_;
  PointLightPass point_light_pass_;
  SpotLightPass spot_light_pass_;  ExposurePass exposure_pass_;
  TonemappingPass tonemapping_pass_;

コンストラクタでspotlightpass_の初期化をします。

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_),
      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) {}

SceneRenderer::RenderにSpotLightPassを追加します。

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

  directional_light_pass_.Render(scene);

  point_light_pass_.Render(scene);

  spot_light_pass_.Render(scene);
  exposure_pass_.SetExposure(0.1f);
  exposure_pass_.Render();

  tonemapping_pass_.Render();
}

実行結果

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

2020 04 07 18 05 04

プログラム全文

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

GitHub: MatchaChoco010/OpenGL-PBR-Map at v14