プログラミング 見習い

プログラミングの勉強の学習日記的なブログ

OpenGLを学ぼうと足掻く Diffuse反射の実装

Diffuse反射というものがあるらしいです。
まあ、全然よくわからないんですが、Diffuse反射には数学モデルがあるので、それに従って実装すればいいでしょう。

Diffuse反射の数学的モデルでは、反射光は以下のような、ある点の法線ベクトルとその点から光源へ向かうベクトルのなす角度  \theta余弦に比例するらしいです。

f:id:Code-C:20140320221844p:plain

この反射光は他にも入射光の輝度と物質の色にも依存するらしいです。

これを式に表すと以下のようになります。

 \vec{I_{d}} = \vec{L_{d}} \vec{K_{d}}(\vec{s} \cdot \vec{n})

 \vec{I_{d}} は反射光の輝度、 \vec{L_{d}} は入射光の輝度、 \vec{K_{d}} は物質の材質(ディフーズ反射率)、 \vec{s} \vec{n} は上の図と同じです。
もちろん余弦をとる必要があるので、 \vec{s} \vec{n} は正規化されている必要があります。
また、 \vec{L_{d}} \vec{K_{d}}内積を行っているわけではなく、単純に各要素同士をかけているだけです。

この式をシェーダに計算させればいいわけですね。
ただ、 \vec{s} \vec{n}内積値が負となる場合、その点には光がまったく当たっていないと考えられるわけですが、反射光が負となることはないので、この値を 0.0 に変えます。
まあこれは単純に  \vec{s} \vec{n}内積と 0.0 の  max をとればいいだけです。

とりあえず適当に作った頂点シェーダを以下に示します。

#version 120
attribute vec4 VertexPosition;
attribute vec3 VertexNormal;
uniform mat4 M, V, P;
uniform mat3 NormalMatrix;
varying vec3 Color;
void main() {
  vec4 LightPosition = vec4(100.0, 20.0, 20.0, 1.0);
  vec3 Kd = vec3(0.6, 0.3, 0.2);
  vec3 Ld = vec3(1.0, 1.0, 1.0);
  mat4 MV = V * M;
  mat4 MVP = P * MV;
  vec3 tnorm = normalize(NormalMatrix * VertexNormal);
  vec4 eyeCoords = MV * VertexPosition;
  vec3 s = normalize(vec3(LightPosition - eyeCoords));
  Color = Ld * Kd * max(dot(s, tnorm), 0.0);
  gl_Position = MVP * VertexPosition;
}

attribute変数として頂点と頂点の法線ベクトルを受け取ります。
uniform変数として M(モデル変換行列)、V(ビュー変換行列)、P(投影変換行列)、法線ベクトルを正しく変換してくれる行列(法線行列)をとります。
varying変数は色情報のColorです。
LightPositionには光源の位置、Kdにはディフューズ反射率、Ldには入射光の輝度を設定しています。
本当ならこれらもuniformにするべきだと思いますが、めんどくさかったのでそうしませんでした。

特に何もいうことはないのですが、ひとつ。
VertexPositionとVertexNormalはローカル座標系の値が渡されるので、これをワールド座標系に変換する必要があるわけです。
もちろんVertexPositionをワールド座標系に変換するためにはモデルビュー行列(ここではM*V)をかければいいだけです。
しかし、ローカル座標系の法線ベクトルであるVertexNormalをワールド座標系に変換するのはそう簡単にはいかないみたいです。
理論はいまいち理解しきれていないのですが、とりあえず一般的に、ローカル座標系の法線ベクトルをワールド座標系に変換するには、モデルビュー行列(M*V)の左上の3x3部分の逆転置行列をかければいいらしいです。
参考:Cg編05::逆行列の転置と法線 - OpenGL de プログラミング


上のシェーダではこの行列をNormalMatrixに渡せばいいというわけです。

GLMでは、mvをモデルビュー行列とすると、

  glm::mat3 m_3x3_inv_transp =
      glm::transpose(glm::inverse(glm::mat3(mv)));

とすれば法線行列が求められるっぽいです。

以下にプログラム全体を示します。

#include <GL/glew.h>
#include <GL/glut.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <cstdlib>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

GLuint vaoHandle;
GLuint programHandle;
GLfloat angle = 0.0;

void InitializeGL();
void DisplayGL();
void TimerGL(int);
void LoadObj(const char* filename,
             std::vector<glm::vec4> &vertices,
             std::vector<glm::vec3> &normals,
             std::vector<GLushort> &elements);

int main(int argc, char *argv[])
{
  glutInit(&argc, argv);
  glutInitWindowSize(500, 500);
  glutInitWindowPosition(10, 10);
  glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);
  glutCreateWindow("OpenGL test01");

  InitializeGL();

  glutDisplayFunc(DisplayGL);
  glutTimerFunc(16, TimerGL, 0);
  glutMainLoop();
}

void InitializeGL()
{
  std::cout << "初期化処理を行います" << std::endl;
  glEnable(GL_DEPTH_TEST);

  std::cout << "メッシュを読み込みます" << std::endl;
  std::vector<glm::vec4> meshVertices;
  std::vector<glm::vec3> meshNormals;
  std::vector<GLushort>  meshElements;
  LoadObj("monkey.obj", meshVertices, meshNormals, meshElements);
  
  std::cout << "GLEWの初期化" << std::endl;
  GLenum glewStatus = glewInit();
  if (glewStatus != GLEW_OK)
  {
    std::cerr << "Error: " << glewGetErrorString(glewStatus) << std::endl;
    std::exit(1);
  }
 
  std::cout << "頂点シェーダの設定" << std::endl;
  
  GLuint vertShader = glCreateShader(GL_VERTEX_SHADER);
  if (0 == vertShader)
  {
    std::cerr << "頂点シェーダ作成エラー" << std::endl;
    std::exit(1);
  }
  const GLchar* vertShaderCode =
      "#version 120\n"
      "attribute vec4 VertexPosition;"
      "attribute vec3 VertexNormal;"
      "uniform mat4 M, V, P;"
      "uniform mat3 NormalMatrix;"
      "varying vec3 Color;"
      "void main() {"
      "vec4 LightPosition = vec4(100.0, 20.0, 20.0, 1.0);"
      "vec3 Kd = vec3(0.6, 0.3, 0.2);"
      "vec3 Ld = vec3(1.0, 1.0, 1.0);"
      "mat4 MV = V * M;"
      "mat4 MVP = P * MV;"
      "vec3 tnorm = normalize(NormalMatrix * VertexNormal);"
      "vec4 eyeCoords = MV * VertexPosition;"
      "vec3 s = normalize(vec3(LightPosition - eyeCoords));"
      "Color = Ld * Kd * max(dot(s, tnorm), 0.0);"
      "gl_Position = MVP * VertexPosition;"
      "}";
  glShaderSource(vertShader, 1, &vertShaderCode, NULL);
  glCompileShader(vertShader);

  GLint result;
  glGetShaderiv(vertShader, GL_COMPILE_STATUS, &result);
  if (GL_FALSE == result)
  {
    std::cerr << "頂点シェーダのコンパイルに失敗しました" << std::endl;
    std::exit(1);
  }

  std::cout << "フラグメントシェーダの設定" << std::endl;
  GLuint fragShader = glCreateShader(GL_FRAGMENT_SHADER);
  if (0 == fragShader)
  {
    std::cerr << "フラグメントシェーダ作成エラー" << std::endl;
    std::exit(1);
  }
  const GLchar* fragShaderCode =
      "#version 120\n"
      "varying vec3 Color;"
      "void main() {"
      "gl_FragColor = vec4(Color, 1.0);"
      "}";
  glShaderSource(fragShader, 1, &fragShaderCode, NULL);
  glCompileShader(fragShader);

  glGetShaderiv(fragShader, GL_COMPILE_STATUS, &result);
  if (GL_FALSE == result)
  {
    std::cerr << "フラグメントシェーダのコンパイルに失敗しました" << std::endl;
    std::exit(1);
  }

  std::cout << "シェーダプログラムの設定" << std::endl;
  programHandle = glCreateProgram();
  if (0 == programHandle)
  {
    std::cerr << "プログラムオブジェクトの作成でエラーがありました" << std::endl;
    std::exit(1);
  }
  glAttachShader(programHandle, vertShader);
  glAttachShader(programHandle, fragShader);

  glLinkProgram(programHandle);

  GLint status;
  glGetProgramiv(programHandle, GL_LINK_STATUS, &status);
  if (GL_FALSE == status)
  {
    std::cerr << "シェーダプログラムのリンクに失敗しました" << std::endl;
    std::exit(1);
  }

  glUseProgram(programHandle);

  GLint attributeVertexPosition;
  attributeVertexPosition =
      glGetAttribLocation(programHandle, "VertexPosition");
  GLint attributeVertexNormal;
  attributeVertexNormal =
      glGetAttribLocation(programHandle, "VertexNormal");

  // VBOの設定
  GLuint vboMeshHandles[2];
  glGenBuffers(2, vboMeshHandles);

  glBindBuffer(GL_ARRAY_BUFFER, vboMeshHandles[0]);
  glBufferData(GL_ARRAY_BUFFER,
               meshVertices.size() * sizeof(meshVertices[0]),
               meshVertices.data(), GL_STATIC_DRAW);
  glBindBuffer(GL_ARRAY_BUFFER, vboMeshHandles[1]);
  glBufferData(GL_ARRAY_BUFFER,
               meshNormals.size() * sizeof(meshNormals[0]),
               meshNormals.data(), GL_STATIC_DRAW);
  
  // IBOの設定
  GLuint iboMeshHandle;
  glGenBuffers(1, &iboMeshHandle);
  
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iboMeshHandle);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, meshElements.size() * sizeof(meshElements[0]),
               meshElements.data(), GL_STATIC_DRAW);

  // VAOの設定
  glGenVertexArrays(1, &vaoHandle);
  glBindVertexArray(vaoHandle);
  
  glEnableVertexAttribArray(attributeVertexPosition);
  glEnableVertexAttribArray(attributeVertexNormal);

  glBindBuffer(GL_ARRAY_BUFFER, vboMeshHandles[0]);
  glVertexAttribPointer(attributeVertexPosition,
                        4,
                        GL_FLOAT,
                        GL_FALSE,
                        0,
                        0
                        );
  glBindBuffer(GL_ARRAY_BUFFER, vboMeshHandles[1]);
  glVertexAttribPointer(attributeVertexNormal,
                        3,
                        GL_FLOAT,
                        GL_FALSE,
                        0,
                        0
                        );
  
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iboMeshHandle);
}

void DisplayGL()
{
  glClearColor(1.0, 1.0, 1.0, 1.0);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  angle += 0.3;
  if (angle >= 360.0)
  {
    angle -= 360.0;
  }
  glm::mat4 rotationMatrix = glm::rotate(glm::mat4(1.0f), angle,
                                         glm::vec3(0.0f, 1.0f, 1.0f));
  
  glm::mat4 lookAt = glm::lookAt(glm::vec3(0.0, 0.0, 10.0),
                                 glm::vec3(0.0, 0.0, 0.0),
                                 glm::vec3(0.0, 1.0, 0.0));
  int screenWidth = glutGet(GLUT_WINDOW_WIDTH);
  int screenHeight = glutGet(GLUT_WINDOW_HEIGHT);
  glm::mat4 projection = glm::perspective(45.0f,
                                          static_cast<float>(screenWidth)/screenHeight,
                                          0.1f,
                                          100.0f);

  GLuint locM = glGetUniformLocation(programHandle, "M");
  glUniformMatrix4fv(locM, 1, GL_FALSE, &rotationMatrix[0][0]);

  GLuint locV = glGetUniformLocation(programHandle, "V");
  glUniformMatrix4fv(locV, 1, GL_FALSE, &lookAt[0][0]);

  GLuint locP = glGetUniformLocation(programHandle, "P");
  glUniformMatrix4fv(locP, 1, GL_FALSE, &projection[0][0]);

  glm::mat4 mv = lookAt * rotationMatrix;
  glm::mat3 m_3x3_inv_transp =
      glm::transpose(glm::inverse(glm::mat3(mv)));
  GLuint locNormalMatrix = glGetUniformLocation(programHandle, "NormalMatrix");
  glUniformMatrix3fv(locNormalMatrix, 1, GL_FALSE, &m_3x3_inv_transp[0][0]);
  
  glBindVertexArray(vaoHandle);
  int size;
  glGetBufferParameteriv(GL_ELEMENT_ARRAY_BUFFER, GL_BUFFER_SIZE, &size);
  glDrawElements(GL_TRIANGLES,
                 size/sizeof(GLushort),
                 GL_UNSIGNED_SHORT,
                 0);

  glutSwapBuffers();
}

void TimerGL(int)
{
  glutPostRedisplay();
  glutTimerFunc(16, TimerGL, 0);
}

void LoadObj(const char* filename,
             std::vector<glm::vec4> &vertices,
             std::vector<glm::vec3> &normals,
             std::vector<GLushort> &elements)
{
  std::ifstream in(filename, std::ios::in);
  if (!in)
  {
    std::cerr << filename << "を開けませんでした" << std::endl;
    std::exit(1);
  }

  std::string line;
  while (getline(in, line))
  {
    if (line.substr(0, 2) == "v ")
    {
      std::istringstream s(line.substr(2));
      glm::vec4 v;
      s >> v.x;
      s >> v.y;
      s >> v.z;
      v.w = 1.0f;
      vertices.push_back(v);
    }
    else if (line.substr(0, 2) == "f ")
    {
      std::istringstream s(line.substr(2));
      GLushort a, b, c;
      s >> a;
      s >> b;
      s >> c;
      a--;
      b--;
      c--;
      elements.push_back(a);
      elements.push_back(b);
      elements.push_back(c);
    }
  }

  normals.resize(vertices.size(), glm::vec3(0.0, 0.0, 0.0));
  for (int i = 0; i < elements.size(); i += 3)
  {
    GLushort ia = elements[i];
    GLushort ib = elements[i+1];
    GLushort ic = elements[i+2];
    glm::vec3 normal = glm::normalize(glm::cross(
        glm::vec3(vertices[ib]) - glm::vec3(vertices[ia]),
        glm::vec3(vertices[ic]) - glm::vec3(vertices[ia])));
    normals[ia] = normals[ib] = normals[ic] = normal;
  }
}

以下のように表示されました。
f:id:Code-C:20140314001103p:plain
はい。
もうダメですね。
こういう書き換え形式ではもう限界です。