OpenGLでDisneyの物理ベースシェーディングを書いてみる

はじめに

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

この記事ではDisneyの物理ベースシェーダーでモデルを表示するところまでを行います。

2020 04 05 20 47 30

Disneyの原則モデル

有名な物理ベースのモデルにDisneyの原則モデルがあります。 次の論文の中で「Disney “principled” BRDF」(Disney 原則BRDF) を提唱しています。

3DCGのソフトウェアに搭載されているマテリアルでPrincipledマテリアルというのは、このディズニーの原則BRDFに基づいたものになります。

リアルタイムレンダリングを行うゲームエンジンなどでも、このDisneyのモデルのサブセットが搭載されていることが

モデルの準備

モデルを準備します。今回はまたUV Sphereを利用します。

2020 04 05 19 57 53

Substance Painterで銅のマテリアルを割り当ててテクスチャを出力します。

2020 04 05 20 46 28

もう一つラフネスを高めにしたものも出力しておきます。

2020 04 05 20 41 23

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

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

Applicationクラスの変更点

anisotropicの値をシェーダに渡すためのUniform Locationを保持します。

application.h
 private:
  GLFWwindow* window_;
  GLuint program_;
  GLuint model_loc_;
  GLuint model_it_loc_;
  GLuint view_projection_loc_;
  GLuint world_camera_position_loc_;
  GLuint emissive_intensity_loc_;
  GLuint anisotropic_loc_;  std::vector<MeshEntity> mesh_entities_;
  std::unique_ptr<Camera> camera_;
application.cpp
  // Uniform変数の位置を取得
  model_loc_ = glGetUniformLocation(program_, "Model");
  model_it_loc_ = glGetUniformLocation(program_, "ModelIT");
  view_projection_loc_ = glGetUniformLocation(program_, "ViewProjection");
  world_camera_position_loc_ =
      glGetUniformLocation(program_, "worldCameraPosition");
  emissive_intensity_loc_ = glGetUniformLocation(program_, "emissiveIntensity");
  anisotropic_loc_ = glGetUniformLocation(program_, "anisotropic");

読み込むモデルとマテリアルを変更します。

application.cpp
  // Meshの読み込み
  auto mesh = Mesh::LoadObjMesh("sphere.obj");
  // Materialの作成
  auto material = std::make_shared<Material>(
      Texture("sphere_BaseColor.png", true),      Texture("sphere_Metallic.png", false),      Texture("sphere_Roughness.png", false),      Texture("sphere_Normal.png", false), Texture("sphere_Emissive.png", true),      0.0f);

MeshEntityの並び順を変えました。

application.cpp
// MeshEntityの作成
  mesh_entities_.emplace_back(mesh, material, glm::vec3(-2.0f, 0.0f, 0.0f),
                              glm::vec3(0.0f), glm::vec3(1.0f));
  mesh_entities_.emplace_back(mesh, material, glm::vec3(0.0f, 0.0f, 0.0f),
                              glm::vec3(0.0f), glm::vec3(1.0f));
  mesh_entities_.emplace_back(mesh, material, glm::vec3(2.0f, 0.0f, 0.0f),
                              glm::vec3(0.0f), glm::vec3(1.0f));

Application::Update内のfor文でanisotropicの値を渡すようにします。

application.cpp
  GLfloat anisotropic = 0.0f;  for (auto&& mesh_entity : mesh_entities_) {
    auto model = mesh_entity.GetModelMatrix();
    auto model_it = glm::inverseTranspose(model);

    glUniformMatrix4fv(model_loc_, 1, GL_FALSE, &model[0][0]);
    glUniformMatrix4fv(model_it_loc_, 1, GL_FALSE, &model_it[0][0]);
    glUniform1f(emissive_intensity_loc_,
                mesh_entity.material_->emissive_intensity_);
    glUniform1f(anisotropic_loc_, anisotropic);    anisotropic += 0.5f;
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D,
                  mesh_entity.material_->albedo_map_.GetTextureId());
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D,
                  mesh_entity.material_->metallic_map_.GetTextureId());
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D,
                  mesh_entity.material_->roughness_map_.GetTextureId());
    glActiveTexture(GL_TEXTURE3);
    glBindTexture(GL_TEXTURE_2D,
                  mesh_entity.material_->normal_map_.GetTextureId());
    glActiveTexture(GL_TEXTURE4);
    glBindTexture(GL_TEXTURE_2D,
                  mesh_entity.material_->emissive_map_.GetTextureId());

    mesh_entity.mesh_->Draw();
  }

これで描画する球体の左から順に0.0、0.5、1.0が渡されます。

シェーダの変更点

頂点シェーダは変わらずです。

フラグメントシェーダを変更します。

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

shader.frag
uniform vec3 worldCameraPosition;
uniform float emissiveIntensity;
uniform float anisotropic;

次にDisneyBRDFの次の実装をGLSLに落とし込みます。

shader.frag
// # Copyright Disney Enterprises, Inc.  All rights reserved.
// #
// # Licensed under the Apache License, Version 2.0 (the "License");
// # you may not use this file except in compliance with the License
// # and the following modification to it: Section 6 Trademarks.
// # deleted and replaced with:
// #
// # 6. Trademarks. This License does not grant permission to use the
// # trade names, trademarks, service marks, or product names of the
// # Licensor and its affiliates, except as required for reproducing
// # the content of the NOTICE file.
// #
// # You may obtain a copy of the License at
// # http://www.apache.org/licenses/LICENSE-2.0
float SchlickFresnel(float cosTheta)
{
  return pow(clamp((1 - cosTheta), 0, 1), 5.0);
}

float D_GTR1(float NdotH, float a)
{
  float a2 = a * a;
  float tmp = 1 + (a2 - 1) * NdotH * NdotH;
  return (a2 - 1) / (PI * log(a2) * tmp);
}

float D_GTR2aniso(float NdotH, float HdotX, float HdotY, float ax, float ay)
{
  float tmp = (HdotX * HdotX) / (ax * ax) + (HdotY * HdotY) / (ay * ay) + NdotH * NdotH;
  return 1 / (PI * ax * ay * tmp * tmp);
}

float G_GGX(float NdotV, float a)
{
  float a2 = a * a;
  float down = NdotV + sqrt(a2 + NdotV * NdotV - a2 * NdotV * NdotV);
  return 1 / down;
}

float G_GGXaniso(float NdotV, float VdotX, float VdotY, float ax, float ay)
{
  float tmp = VdotX * VdotX * ax * ax + VdotY * VdotY * ay * ay + NdotV * NdotV;
  float down = NdotV + sqrt(tmp);
  return 1 / down;
}

vec3 DisneyBRDF(vec3 L, vec3 V, vec3 N, vec3 H, vec3 X, vec3 Y, vec3 baseColor, float subsurface, float metallic, float specular, float specularTint, float roughness, float anisotropic, float sheen, float sheenTint, float clearcoat, float clearcoatGloss)
{
  float NdotL = dot(N, L);
  float NdotV = dot(N, V);
  if (NdotL < 0 || NdotV < 0) return vec3(0);

  float NdotH = dot(N, H);
  float LdotH = dot(L, H);

  float luminance = 0.3 * baseColor.r + 0.6 * baseColor.g + 0.1 * baseColor.b;
  vec3 C_tint = luminance > 0 ? baseColor / luminance : vec3(1);
  vec3 C_spec = mix(specular * 0.08 * mix(vec3(1), C_tint, specularTint), baseColor, metallic);
  vec3 C_sheen = mix(vec3(1), C_tint, sheenTint);

  // diffuse
  float F_i = SchlickFresnel(NdotL);
  float F_o = SchlickFresnel(NdotV);
  float F_d90 = 0.5 + 2 * LdotH * LdotH * roughness;
  float F_d = mix(1.0, F_d90, F_i) * mix(1.0, F_d90, F_o);

  float F_ss90 = LdotH * LdotH * roughness;
  float F_ss = mix(1.0, F_ss90, F_i) * mix(1.0, F_ss90, F_o);
  float ss = 1.25 * (F_ss * (1 / (NdotL + NdotV) - 0.5) + 0.5);

  float FH = SchlickFresnel(LdotH);
  vec3 F_sheen = FH * sheen * C_sheen;

  vec3 BRDFdiffuse = ((1 / PI) * mix(F_d, ss, subsurface) * baseColor + F_sheen) * (1 - metallic);

  // specular
  float aspect = sqrt(1 - anisotropic * 0.9);
  float roughness2 = roughness * roughness;
  float a_x = max(0.001, roughness2 / aspect);
  float a_y = max(0.001, roughness2 * aspect);
  float D_s = D_GTR2aniso(NdotH, dot(H, X), dot(H, Y), a_x, a_y);
  vec3 F_s = mix(C_spec, vec3(1), FH);
  float G_s = G_GGXaniso(NdotL, dot(L, X), dot(L, Y), a_x, a_y) * G_GGXaniso(NdotV, dot(V, X), dot(V, Y), a_x, a_y);

  vec3 BRDFspecular = G_s * F_s * D_s;

  // clearcoat
  float D_r = D_GTR1(NdotH, mix(0.1, 0.001, clearcoatGloss));
  float F_r = mix(0.04, 1.0, FH);
  float G_r = G_GGX(NdotL, 0.25) * G_GGX(NdotV, 0.25);

  vec3 BRDFclearcoat = vec3(0.25 * clearcoat * G_r * F_r * D_r);

  return BRDFdiffuse + BRDFspecular + BRDFclearcoat;
}

main関数内でtangentとbitangentを計算します。

shader.frag
  vec3 tangent = normalize(vWorldTangent);
  vec3 bitangent = normalize(cross(N, tangent));
  tangent = normalize(cross(bitangent, N));

パラメータをシェーダにベタ書きします。

shader.frag
  float subsurface = 0.0;
  float specular = 0.5;
  float specularTint = 0.0;
  float sheen = 0.0;
  float sheenTint = 0.5;
  float clearcoat = 0.0;
  float clearcoatGloss = 1.0;

ライティングで利用するBRDFをDisneyBRDFに入れ替えます。

shader.frag
  for (int i = 0; i < 3; i++) {
    vec3 L = normalize(worldLightPositions[i] - vWorldPosition);
    vec3 H = normalize(L + V);
    float attenuation =
      DistanceAttenuation(distance(worldLightPositions[i], vWorldPosition), 20.0);
    vec3 irradiance =
      lightIntensities[i] / (4.0 * PI) * attenuation * sRGBToACES(lightColors[i]) * max(dot(N, L), 0);
    // color += BRDF(albedo, metallic, roughness, N, V, L, H) * irradiance;    color += DisneyBRDF(L, V, N, H, tangent, bitangent, albedo, subsurface, metallic, specular, specularTint, roughness, anisotropic, sheen, sheenTint, clearcoat, clearcoatGloss) * irradiance;  }

クリアコートの値などDisneyのモデルで追加されるパラメータは今回はシェーダに直接書いています。 本来ならばこれらのパラメータもUniform変数やテクスチャを通して渡せるようにしたほうが良いでしょう。 また、anisotropicのXY方向をUVのタンジェントとバイタンジェント方向にしていますが、実用するにはanisotropy angleをテクスチャで渡してやるなどしたほうが良さげです。

実行結果

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

2020 04 05 20 47 30

anisotropicの値によってハイライトが引き伸ばされているのが確認できます。

ラフネス高めのテクスチャで実行すると次の通り。

2020 04 05 20 42 12

ラフネス高めでClearcoatを1.0にすると次の通り。

shader.frag
  float subsurface = 0.0;
  float specular = 0.5;
  float specularTint = 0.0;
  float sheen = 0.0;
  float sheenTint = 0.5;
  // float clearcoat = 0.0;
  float clearcoat = 1.0;
  float clearcoatGloss = 0.95;

2020 04 05 20 43 57

内側の層の粗いラフネスのぼやけたハイライトと、外側の層のクリアコートによる鋭いハイライトが確認できました。

プログラム全文

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

GitHub: MatchaChoco010/OpenGL-PBR-Map at v9